Compare commits

..

8 Commits

Author SHA1 Message Date
Martín (Netux) Rodríguez 753b08edf0 Update global audio volume exclusively through useReplContext effect
This ensures the audio volume is synced across tabs, since changing the volume in one tab would retrigger the effect in the other tabs.

It also means we can remove the explicit call to `setGlobalAudioVolume()` from <SettingsTab>
2024-09-03 18:24:48 -03:00
Martín (Netux) Rodríguez ccb1256aa2 Adjust settings number slider input width according to max contents
This is a bit overkill, but this way we can somewhat adjust the width of the input so no symbols are cut off (either by the input being too small, or the up/down arrows from the browser).

I am not happy about having to use `calc()` with some magic numbers either, but this is what looked nicest to me and had the least effort.
2024-09-03 18:21:04 -03:00
Martín (Netux) Rodríguez 657d89cb98 Fix settings number slider browser up arrow not increasing the value correctly when step < 1
If we keep flooring the value, but have a step of, e.g., 0.1, then we are never going to reach the next whole number value.

This applies specifically to the Audio Volume slider.
2024-09-03 18:12:42 -03:00
Martín (Netux) Rodríguez 5797c42753 Calculate global gain with a better approximation of human perceived loudness
Taken from https://www.dr-lex.be/info-stuff/volumecontrols.html
2024-09-03 17:58:52 -03:00
Martín (Netux) Rodríguez afa55535ba Disable Audio Volume slider when Audio Engine Target is OSC 2024-09-03 15:42:41 -03:00
Martín (Netux) Rodríguez 1c94074ed6 Run codeformat 2024-09-03 15:23:20 -03:00
Martín (Netux) Rodríguez d2b6b257c0 Merge remote-tracking branch 'tidalcycles/main' into global-volume-slider 2024-09-03 14:40:03 -03:00
Martín (Netux) Rodríguez c6e1f758be Add Audio Volume setting to REPL
Allows for adjusting the global volume/gain of the REPL. Set default global value to 50%.

Acts similarily to `gain()`, but without modifying the code, so passerbys who don't know how to use the tool can adjust the volume too).

The volume slider uses a logarithmic scale, so it adjusts better to human sound perception.
2024-08-18 02:14:03 -03:00
49 changed files with 10057 additions and 12753 deletions
-1
View File
@@ -23,4 +23,3 @@ reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
packages/hs2js/src/hs2js.mjs
samples
-1
View File
@@ -29,7 +29,6 @@ const editor = new StrudelMirror({
import('@strudel/core'),
import('@strudel/draw'),
import('@strudel/mini'),
import('@strudel/shader'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
);
+7 -7
View File
@@ -56,18 +56,18 @@
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^2.1.3",
"acorn": "^8.13.0",
"@vitest/ui": "^1.1.0",
"acorn": "^8.11.3",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import": "^2.29.1",
"events": "^3.3.0",
"jsdoc": "^4.0.3",
"jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^8.1.8",
"prettier": "^3.3.3",
"lerna": "^8.0.1",
"prettier": "^3.1.1",
"rollup-plugin-visualizer": "^5.12.0",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+4 -2
View File
@@ -99,8 +99,10 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-.',
preventDefault: true,
run: () => onStop?.(),
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
/* {
key: 'Ctrl-Shift-.',
+4 -5
View File
@@ -454,6 +454,9 @@ export const { drive } = registerControl('drive');
*/
export const { channels, ch } = registerControl('channels', 'ch');
// superdirt only
export const { phaserrate, phasr } = registerControl('phaserrate', 'phasr');
/**
* Phaser audio effect that approximates popular guitar pedals.
*
@@ -465,11 +468,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
* .phaser("<1 2 4 8>")
*
*/
export const { phaserrate, ph, phaser } = registerControl(
['phaserrate', 'phaserdepth', 'phasercenter', 'phasersweep'],
'ph',
'phaser',
);
export const { phaser, ph } = registerControl(['phaser', 'phaserdepth', 'phasercenter', 'phasersweep'], 'ph');
/**
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
+1 -1
View File
@@ -36,6 +36,6 @@
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+31 -107
View File
@@ -809,6 +809,15 @@ export class Pattern {
//////////////////////////////////////////////////////////////////////
// Multi-pattern functions
/**
* Stacks the given pattern(s) to the current pattern.
* @name stack
* @memberof Pattern
* @example
* s("hh*4").stack(
* note("c4(5,8)")
* )
*/
stack(...pats) {
return stack(this, ...pats);
}
@@ -817,9 +826,30 @@ export class Pattern {
return sequence(this, ...pats);
}
/**
* Appends the given pattern(s) to the current pattern.
* @name seq
* @memberof Pattern
* @synonyms sequence, fastcat
* @example
* s("hh*4").seq(
* note("c4(5,8)")
* )
*/
seq(...pats) {
return sequence(this, ...pats);
}
/**
* Appends the given pattern(s) to the next cycle.
* @name cat
* @memberof Pattern
* @synonyms slowcat
* @example
* s("hh*4").cat(
* note("c4(5,8)")
* )
*/
cat(...pats) {
return cat(this, ...pats);
}
@@ -1250,12 +1280,6 @@ export function reify(thing) {
* @example
* stack("g3", "b3", ["e4", "d4"]).note()
* // "g3,b3,[e4,d4]".note()
*
* @example
* // As a chained function:
* s("hh*4").stack(
* note("c4(5,8)")
* )
*/
export function stack(...pats) {
// Array test here is to avoid infinite recursions..
@@ -1384,11 +1408,6 @@ export function slowcatPrime(...pats) {
* cat("e5", "b4", ["d5", "c5"]).note()
* // "<e5 b4 [d5 c5]>".note()
*
* @example
* // As a chained function:
* s("hh*4").cat(
* note("c4(5,8)")
* )
*/
export function cat(...pats) {
return slowcat(...pats);
@@ -1411,36 +1430,6 @@ export function arrange(...sections) {
return s_cat(...sections).slow(total);
}
/**
* Similarly to `arrange`, allows you to arrange multiple patterns together over multiple cycles.
* Unlike `arrange`, you specify a start and stop time for each pattern rather than duration, which
* means that patterns can overlap.
* @return {Pattern}
* @example
seqPLoop([0, 2, "bd(3,8)"],
[1, 3, "cp(3,8)"]
)
.sound()
*/
export function seqPLoop(...parts) {
let total = Fraction(0);
const pats = [];
for (let part of parts) {
if (part.length == 2) {
part.unshift(total);
}
total = part[1];
}
return stack(
...parts.map(([start, stop, pat]) =>
pure(reify(pat)).compress(Fraction(start).div(total), Fraction(stop).div(total)),
),
)
.slow(total)
.innerJoin(); // or resetJoin or restartJoin ??
}
export function fastcat(...pats) {
let result = slowcat(...pats);
if (pats.length > 1) {
@@ -1459,18 +1448,12 @@ export function sequence(...pats) {
}
/** Like **cat**, but the items are crammed into one cycle.
* @synonyms sequence, fastcat
* @synonyms fastcat, sequence
* @example
* seq("e5", "b4", ["d5", "c5"]).note()
* // "e5 b4 [d5 c5]".note()
*
* @example
* // As a chained function:
* s("hh*4").seq(
* note("c4(5,8)")
* )
*/
export function seq(...pats) {
return fastcat(...pats);
}
@@ -2033,34 +2016,6 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
return pat.zoom(a.begin, a.end);
});
/**
* Splits a pattern into the given number of slices, and plays them according to a pattern of slice numbers.
* Similar to `slice`, but slices up patterns rather than sound samples.
* @param {number} number of slices
* @param {number} slices to play
* @example
* note("0 1 2 3 4 5 6 7".scale('c:mixolydian'))
*.bite(4, "3 2 1 0")
* @example
* sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]")
.bank("RolandTR909").speed(1.2)
.bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3")
*/
export const bite = register(
'bite',
(npat, ipat, pat) => {
return ipat
.fmap((i) => (n) => {
const a = Fraction(i).div(n).mod(1);
const b = a.add(Fraction(1).div(n));
return pat.zoom(a, b);
})
.appLeft(npat)
.squeezeJoin();
},
false,
);
/**
* Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle.
* @param {number} fraction fraction to select
@@ -2492,37 +2447,6 @@ Pattern.prototype.tag = function (tag) {
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
};
/**
* Filters haps using the given function
* @name filter
* @param {Function} test function to test Hap
* @example
* s("hh!7 oh").filter(hap => hap.value.s==='hh')
*/
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
/**
* Filters haps by their begin time
* @name filterWhen
* @noAutocomplete
* @param {Function} test function to test Hap.whole.begin
*/
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
/**
* Use within to apply a function to only a part of a pattern.
* @name within
* @param {number} start start within cycle (0 - 1)
* @param {number} end end within cycle (0 - 1). Must be > start
* @param {Function} func function to be applied to the sub-pattern
*/
export const within = register('within', (a, b, fn, pat) =>
stack(
fn(pat.filterWhen((t) => t.cyclePos() >= a && t.cyclePos() <= b)),
pat.filterWhen((t) => t.cyclePos() < a || t.cyclePos() > b),
),
);
//////////////////////////////////////////////////////////////////////
// Tactus-related functions, i.e. ones that do stepwise
// transformations
+1 -6
View File
@@ -169,18 +169,13 @@ const refs = {};
export async function midin(input) {
if (isPattern(input)) {
throw new Error(
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`,
);
}
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
+1 -1
View File
@@ -37,6 +37,6 @@
"devDependencies": {
"peggy": "^3.0.2",
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
-1
View File
@@ -12,7 +12,6 @@ export async function prebake() {
import('@strudel/webaudio'),
import('@strudel/codemirror'),
import('@strudel/hydra'),
import('@strudel/shader'),
import('@strudel/soundfonts'),
import('@strudel/midi'),
// import('@strudel/xen'),
+2 -2
View File
@@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
if (!(name in writeMessagers)) {
getWriter(name, br);
}
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
const onTrigger = (time, hap, currentTime) => {
var message = '';
var chk = 0;
if (typeof hap.value === 'object') {
@@ -105,7 +105,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
} else {
message = hap.value;
}
const offset = (targetTime - currentTime + latency) * 1000;
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(function () {
writeMessagers[name](message, chk);
-16
View File
@@ -1,16 +0,0 @@
# @strudel/shader
Helpers for drawing shader.
## Todos
Here are the things that needs to be implemented:
- [ ] Shader compilation error reporting, e.g. to show the line number
- [ ] Shader import from url, like shadertoy or git
- [ ] Display shader author attribution, e.g. to respect CC-BY
- [ ] Handle WebGL context lost by restoring the objects.
- [ ] Multiple instances and custom canvas positions
- [ ] Multiple programs, to be swapped like a pattern
- [ ] Texture inputs
- [ ] Buffer inputs, e.g. to generate a texture
-1
View File
@@ -1 +0,0 @@
export * from './shader.mjs';
-40
View File
@@ -1,40 +0,0 @@
{
"name": "@strudel/shader",
"version": "1.0.0",
"description": "Helpers for drawing shader",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave",
"shader"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"picogl": "^0.17.9"
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
}
}
-347
View File
@@ -1,347 +0,0 @@
/*
shader.mjs - implements the `loadShader` function
Copyright (C) 2024 Strudel contributors
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
/// Here is a feature demo
// Setup a shader
let truchetFTW = fetch('https://raw.githubusercontent.com/TristanCacqueray/shaders/refs/heads/main/shaders/Truchet%20%2B%20Kaleidoscope%20FTW.glsl').then((res) => res.text())
// This shader provides the following uniforms:
// uniform float icolor;
// uniform float moveFWD;
// uniform float rotations[4];
// uniform float modulations[6];
// Start the instance and binds the uniforms
let {uniforms} = await loadShader(truchetFTW)
setcpm(96)
// A smoothing function that is called for each frame
let smooth = (desired, speed) => (value) => value + ((desired - value) / speed)
// Each kick updates a different rotation value.
let rotationIndex = 0
$: s("bd").bank("RolandTR808")
.gain(2).dist("<1 .7 .7 .7>")
.mask("<1@30 0@2>")
.onTrigger(() => uniforms.rotations.set(
cur => smooth(cur + 1, 20), rotationIndex++),
false)
// Each hat increase the icolor value.
$: sound("hh*4").bank("RolandTR808")
.room(.3).gain(".25 .3 .4")
.mask("<0@8 1@32>")
.onTrigger(() => uniforms.icolor.incr(0.1), false)
// The snare smoothly increase the moveFWD value
$: s("cp/8").bank("RolandTR808")
.hpf(500).hpa(.8).hpenv("<-3 -2 -3 -2 -1>/8")
.room(0.5).roomsize(7).rlp(5000).gain(.2)
.onTrigger(() => uniforms.moveFWD.set(cur => smooth(cur + 1, 30)), false)
// Each piano note updates a different modulations value
let pianoPitches = {}
$: note("<C D G A Bb D C A G D Bb A>*[2,2.02]")
.clip(1.1)
.transpose("<-12 -24 -12 0>/8")
// .sound("sawtooth")
.sound("triangle")
.cutoff(perlin.slow(5).range(20,1200))
.room(.8).roomsize(.6)
.gain(.4)
.onTrigger((_, hap) => {
const n = hap.value.note
// assign unique array position for each new notes
if (!pianoPitches[n]) pianoPitches[n] = Object.keys(pianoPitches).length + 1
const idx = pianoPitches[n]
uniforms.modulations.set(cur => smooth(cur + .5, 55), idx)
}, false)
*/
import { logger } from '@strudel/core';
// The standard fullscreen vertex shader.
const vertexShader = `#version 300 es
precision highp float;
layout(location=0) in vec2 position;
void main() {
gl_Position = vec4(position, 1, 1);
}
`;
// Make the fragment source, similar to the one from shadertoy.
function mkFragmentShader(code) {
return `#version 300 es
precision highp float;
out vec4 oColor;
uniform float iTime;
uniform vec2 iResolution;
#define STRUDEL 1
${code}
void main(void) {
mainImage(oColor, gl_FragCoord.xy);
}
`;
}
// Helper class to handle uniform updates
class UniformValue {
constructor(count, draw) {
this.draw = draw;
this.value = new Array(count).fill(0);
this.frameModifier = new Array(count).fill(null);
}
// Helper to perform a simple increment
incr(value, pos = 0) {
const idx = pos % this.value.length;
this.value[idx] += value;
this.frameModifier[idx] = null;
this.draw();
}
// The value can be a function that will be called for each rendering frame
set(value, pos = 0) {
const idx = pos % this.value.length;
if (typeof value === 'function') {
this.frameModifier[idx] = value(this.value[idx]);
} else {
this.value[idx] = value;
this.frameModifier[idx] = null;
}
this.draw();
}
get(pos = 0) {
return this.value[pos % this.value.length];
}
// This function is called for every frame, allowing to run a smooth modifier
_frameUpdate(elapsed) {
this.value = this.value.map((value, idx) =>
this.frameModifier[idx] ? this.frameModifier[idx](value, elapsed) : value,
);
return this.value;
}
// When the shader is update, this function adjust the number of values, preserving the current one
_resize(count) {
if (count != this.count) {
count = Math.max(1, count);
resizeArray(this.value, count, 0);
resizeArray(this.frameModifier, count, null);
}
}
}
// Shrink or extend an array
function resizeArray(arr, size, defval) {
if (arr.length > size) arr.length = size;
else arr.push(...new Array(size - arr.length).fill(defval));
}
// Setup the instance's uniform after shader compilation.
function setupUniforms(instance) {
const newUniforms = new Set();
const draw = () => {
// Start the drawing loop
instance.age = 0;
if (!instance.drawing) {
instance.drawing = requestAnimationFrame(instance.update);
}
};
// Collect every available uniforms
let gl = instance.gl;
const numUniforms = instance.gl.getProgramParameter(instance.program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < numUniforms; ++i) {
const inf = gl.getActiveUniform(instance.program, i);
// Arrays have a `[0]` suffix in their name, drop that
const name = inf.name.replace('[0]', '');
// Figure out how many values is this uniform, and how to update it.
let count = inf.size;
let updateFunc = 'uniform1fv';
switch (inf.type) {
case gl.FLOAT_VEC2:
count *= 2;
updateFunc = 'uniform2fv';
break;
case gl.FLOAT_VEC3:
count *= 3;
updateFunc = 'uniform3fv';
break;
case gl.FLOAT_VEC4:
count *= 4;
updateFunc = 'uniform4fv';
break;
}
// This is a new uniform
if (!instance.uniforms[name]) instance.uniforms[name] = new UniformValue(count, draw);
// This is a known uniform, make sure it's size is correct
else instance.uniforms[name]._resize(count);
// Record it's location for the 'updateUniforms' below.
instance.uniforms[name].loc = gl.getUniformLocation(instance.program, inf.name);
instance.uniforms[name].updateFunc = updateFunc;
// Record the name so that unused uniform can be deleted below
newUniforms.add(name);
}
// Remove deleted uniforms
Object.keys(instance.uniforms).forEach((name) => {
if (!newUniforms.has(name)) delete instance.uniforms[name];
});
}
// Update the uniforms for a given drawFrame call.
function updateUniforms(gl, now, elapsed, uniforms) {
Object.entries(uniforms).forEach(([name, uniform]) => {
try {
if (name == 'iTime') {
gl.uniform1f(uniform.loc, now);
} else if (name == 'iResolution') {
gl.uniform2f(uniform.loc, gl.canvas.width, gl.canvas.height);
} else {
const value = uniform._frameUpdate(elapsed);
// Send the value to the GPU
// console.log('updateUniforms:', name, uniform.updateFunc, value);
gl[uniform.updateFunc](uniform.loc, value);
}
} catch (err) {
console.warn('uniform error');
console.error(err);
}
});
}
// Setup the canvas and return the WebGL context.
function setupCanvas(name) {
// TODO: support custom size
const width = 400;
const height = 300;
const canvas = document.createElement('canvas');
canvas.id = 'cnv-' + name;
canvas.width = width;
canvas.height = height;
const top = 60 + Object.keys(_instances).length * height;
canvas.style = `pointer-events:none;width:${width}px;height:${height}px;position:fixed;top:${top}px;right:23px`;
document.body.append(canvas);
return canvas.getContext('webgl2');
}
function createProgram(gl, vertex, fragment) {
const compile = (type, source) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
const err = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw err;
}
return shader;
};
const program = gl.createProgram();
gl.attachShader(program, compile(gl.VERTEX_SHADER, vertex));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragment));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const err = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw err;
}
gl.useProgram(program);
return program;
}
// Setup the shader instance
function initializeShaderInstance(name, code) {
const gl = setupCanvas(name);
// Two triangle to cover the whole canvas
const mkPositionArray = () => {
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(buf, 2, gl.FLOAT, false, 0, 0);
return vao;
};
try {
let array = mkPositionArray();
let program = createProgram(gl, vertexShader, code);
const instance = { gl, code, program, array, uniforms: {} };
setupUniforms(instance);
// Render frame logic
let prev = performance.now() / 1000;
instance.age = 0;
instance.update = () => {
const now = performance.now() / 1000;
const elapsed = instance.age == 0 ? 1 / 60 : now - prev;
prev = now;
// console.log('drawing!');
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindVertexArray(array);
// Send the uniform values to the GPU
updateUniforms(instance.gl, now, elapsed, instance.uniforms);
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// After sometime, if no update happened, stop the animation loop to save cpu cycles
if (instance.age++ < 100) requestAnimationFrame(instance.update);
else instance.drawing = false;
};
instance.update();
return instance;
} catch (err) {
gl.canvas.remove();
throw err;
}
}
// Update the instance program
function reloadShaderInstanceCode(instance, code) {
const program = createProgram(instance.gl, vertexShader, code);
instance.gl.deleteProgram(instance.program);
instance.program = program;
instance.code = code;
setupUniforms(instance);
}
// Keep track of the running shader instances
let _instances = {};
export function loadShader(code = '', name = 'default') {
if (code) {
code = mkFragmentShader(code);
}
if (!_instances[name]) {
_instances[name] = initializeShaderInstance(name, code);
logger('[shader] ready');
} else if (_instances[name].code != code) {
reloadShaderInstanceCode(_instances[name], code);
logger('[shader] reloaded');
}
return _instances[name];
}
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+49 -62
View File
@@ -22,13 +22,16 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getSampleInfo(hapValue, bank) {
const { s, n = 0, speed = 1.0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => {
let transpose = 0;
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
}
let midi = valueToMidi({ freq, note }, 36);
transpose = midi - 36; // C3 is middle C
const ac = getAudioContext();
let sampleUrl;
let index = 0;
if (Array.isArray(bank)) {
@@ -47,54 +50,19 @@ export function getSampleInfo(hapValue, bank) {
index = getSoundIndex(n, bank[closest].length);
sampleUrl = bank[closest][index];
}
const label = `${s}:${index}`;
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, sampleUrl, index, midi, label, playbackRate };
}
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
const ac = getAudioContext();
const buffer = await loadBuffer(sampleUrl, ac, label);
if (hapValue.unit === 'c') {
playbackRate = playbackRate * buffer.duration;
}
return { buffer, playbackRate };
};
// creates playback ready AudioBufferSourceNode from hapValue
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
if (hapValue.speed < 0) {
let buffer = await loadBuffer(sampleUrl, ac, s, index);
if (speed < 0) {
// should this be cached?
buffer = reverseBuffer(buffer);
}
const ac = getAudioContext();
const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer;
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
bufferSource.playbackRate.value = playbackRate;
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
const sliceDuration = (end - begin) * bufferDuration;
return { bufferSource, offset, bufferDuration, sliceDuration };
return bufferSource;
};
export const loadBuffer = (url, ac, s, n = 0) => {
@@ -264,10 +232,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const { prebake, tag } = options;
processSampleMap(
sampleMap,
(key, bank) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
(key, value) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: bank,
samples: value,
baseUrl,
prebake,
tag,
@@ -281,26 +249,38 @@ const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
let {
s,
freq,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = undefined, // if set, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
loopBegin = 0,
begin = 0,
loopEnd = 1,
end = 1,
duration,
} = value;
// load sample
if (speed === 0) {
// no playback
return;
}
loop = s.startsWith('wt_') ? 1 : value.loop;
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
// vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
// asny stuff above took too long?
if (ac.currentTime > t) {
@@ -312,19 +292,26 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
return;
}
// vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
const time = t + nudge;
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps;
}
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
bufferSource.start(time, offset);
const envGain = ac.createGain();
const node = bufferSource.connect(envGain);
// if none of these controls is set, the duration of the sound will be set to the duration of the sample slice
if (clip == null && loop == null && value.release == null) {
duration = sliceDuration;
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
duration = (end - begin) * bufferDuration;
}
let holdEnd = t + duration;
+9 -2
View File
@@ -147,6 +147,14 @@ export function initializeAudioOutput() {
destinationGain.connect(audioContext.destination);
}
export function setGlobalGain(gain) {
if (destinationGain == null) {
initializeAudioOutput();
}
destinationGain.gain.value = gain;
}
// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
@@ -375,7 +383,7 @@ export const superdough = async (value, t, hapDuration) => {
bandq = getDefaultValue('bandq'),
channels = getDefaultValue('channels'),
//phaser
phaserrate: phaser,
phaser,
phaserdepth = getDefaultValue('phaserdepth'),
phasersweep,
phasercenter,
@@ -421,7 +429,6 @@ export const superdough = async (value, t, hapDuration) => {
};
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
}
// get source AudioNode
+1 -1
View File
@@ -37,6 +37,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+1 -1
View File
@@ -38,6 +38,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+1 -1
View File
@@ -34,6 +34,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+9265 -11504
View File
File diff suppressed because it is too large Load Diff
+176 -383
View File
@@ -967,156 +967,6 @@ exports[`runs examples > example "begin" example index 0 1`] = `
]
`;
exports[`runs examples > example "bite" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:Bb3 ]",
"[ 1/8 → 1/4 | note:C4 ]",
"[ 1/4 → 3/8 | note:G3 ]",
"[ 3/8 → 1/2 | note:A3 ]",
"[ 1/2 → 5/8 | note:E3 ]",
"[ 5/8 → 3/4 | note:F3 ]",
"[ 3/4 → 7/8 | note:C3 ]",
"[ 7/8 → 1/1 | note:D3 ]",
"[ 1/1 → 9/8 | note:Bb3 ]",
"[ 9/8 → 5/4 | note:C4 ]",
"[ 5/4 → 11/8 | note:G3 ]",
"[ 11/8 → 3/2 | note:A3 ]",
"[ 3/2 → 13/8 | note:E3 ]",
"[ 13/8 → 7/4 | note:F3 ]",
"[ 7/4 → 15/8 | note:C3 ]",
"[ 15/8 → 2/1 | note:D3 ]",
"[ 2/1 → 17/8 | note:Bb3 ]",
"[ 17/8 → 9/4 | note:C4 ]",
"[ 9/4 → 19/8 | note:G3 ]",
"[ 19/8 → 5/2 | note:A3 ]",
"[ 5/2 → 21/8 | note:E3 ]",
"[ 21/8 → 11/4 | note:F3 ]",
"[ 11/4 → 23/8 | note:C3 ]",
"[ 23/8 → 3/1 | note:D3 ]",
"[ 3/1 → 25/8 | note:Bb3 ]",
"[ 25/8 → 13/4 | note:C4 ]",
"[ 13/4 → 27/8 | note:G3 ]",
"[ 27/8 → 7/2 | note:A3 ]",
"[ 7/2 → 29/8 | note:E3 ]",
"[ 29/8 → 15/4 | note:F3 ]",
"[ 15/4 → 31/8 | note:C3 ]",
"[ 31/8 → 4/1 | note:D3 ]",
]
`;
exports[`runs examples > example "bite" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 0/1 → 1/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 1/16 → 1/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 1/8 → 1/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/8 → 1/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 3/16 → 1/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 1/4 → 5/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 9/32 → 5/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 5/16 → 11/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 5/16 → 3/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/8 → 7/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/8 → 1/2 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 13/32 → 7/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 7/16 → 1/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/2 → 5/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/2 → 5/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 9/16 → 5/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 5/8 → 3/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 5/8 → 3/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 11/16 → 3/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 3/4 → 25/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 3/4 → 13/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 13/16 → 7/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 27/32 → 7/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 7/8 → 15/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 7/8 → 1/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 29/32 → 15/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 15/16 → 1/1 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/1 → 9/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/1 → 9/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 17/16 → 9/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 9/8 → 5/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 9/8 → 5/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 19/16 → 5/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 5/4 → 21/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 41/32 → 21/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 21/16 → 43/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 21/16 → 11/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 11/8 → 23/16 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 11/8 → 3/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/2 → 13/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/2 → 13/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 25/16 → 13/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 13/8 → 7/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 13/8 → 7/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 27/16 → 7/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 7/4 → 57/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 7/4 → 29/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 29/16 → 15/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 59/32 → 15/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 15/8 → 31/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 15/8 → 2/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 61/32 → 31/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 31/16 → 2/1 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 2/1 → 17/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 2/1 → 17/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 33/16 → 17/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 17/8 → 9/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 17/8 → 9/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 35/16 → 9/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 9/4 → 37/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 73/32 → 37/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 37/16 → 75/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 37/16 → 19/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 19/8 → 39/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 19/8 → 5/2 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 77/32 → 39/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 39/16 → 5/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 5/2 → 21/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 5/2 → 21/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 41/16 → 21/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 21/8 → 11/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 21/8 → 11/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 43/16 → 11/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 11/4 → 89/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 11/4 → 45/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 45/16 → 23/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 91/32 → 23/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 23/8 → 47/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 23/8 → 3/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 93/32 → 47/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 47/16 → 3/1 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/1 → 25/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/1 → 25/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 49/16 → 25/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 25/8 → 13/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 25/8 → 13/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 51/16 → 13/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 13/4 → 53/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 105/32 → 53/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 53/16 → 107/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 53/16 → 27/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 27/8 → 55/16 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 27/8 → 7/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 7/2 → 29/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 7/2 → 29/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 57/16 → 29/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 29/8 → 15/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 29/8 → 15/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 59/16 → 15/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 15/4 → 121/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 15/4 → 61/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 61/16 → 31/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 123/32 → 31/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 31/8 → 63/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 31/8 → 4/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 125/32 → 63/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 63/16 → 4/1 | s:bd bank:RolandTR909 speed:1.2 ]",
]
`;
exports[`runs examples > example "bpattack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
@@ -1379,16 +1229,6 @@ exports[`runs examples > example "brandBy" example index 0 1`] = `
`;
exports[`runs examples > example "cat" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:e5 ]",
"[ 1/1 → 2/1 | note:b4 ]",
"[ 2/1 → 5/2 | note:d5 ]",
"[ 5/2 → 3/1 | note:c5 ]",
"[ 3/1 → 4/1 | note:e5 ]",
]
`;
exports[`runs examples > example "cat" example index 1 1`] = `
[
"[ 0/1 → 1/4 | s:hh ]",
"[ 1/4 → 1/2 | s:hh ]",
@@ -1411,6 +1251,16 @@ exports[`runs examples > example "cat" example index 1 1`] = `
]
`;
exports[`runs examples > example "cat" example index 0 2`] = `
[
"[ 0/1 → 1/1 | note:e5 ]",
"[ 1/1 → 2/1 | note:b4 ]",
"[ 2/1 → 5/2 | note:d5 ]",
"[ 5/2 → 3/1 | note:c5 ]",
"[ 3/1 → 4/1 | note:e5 ]",
]
`;
exports[`runs examples > example "ceil" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:42 ]",
@@ -2816,43 +2666,6 @@ exports[`runs examples > example "fastGap" example index 0 1`] = `
]
`;
exports[`runs examples > example "filter" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 1/8 → 1/4 | s:hh ]",
"[ 1/4 → 3/8 | s:hh ]",
"[ 3/8 → 1/2 | s:hh ]",
"[ 1/2 → 5/8 | s:hh ]",
"[ 5/8 → 3/4 | s:hh ]",
"[ 3/4 → 7/8 | s:hh ]",
"[ 7/8 → 1/1 | s:oh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 9/8 → 5/4 | s:hh ]",
"[ 5/4 → 11/8 | s:hh ]",
"[ 11/8 → 3/2 | s:hh ]",
"[ 3/2 → 13/8 | s:hh ]",
"[ 13/8 → 7/4 | s:hh ]",
"[ 7/4 → 15/8 | s:hh ]",
"[ 15/8 → 2/1 | s:oh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 17/8 → 9/4 | s:hh ]",
"[ 9/4 → 19/8 | s:hh ]",
"[ 19/8 → 5/2 | s:hh ]",
"[ 5/2 → 21/8 | s:hh ]",
"[ 21/8 → 11/4 | s:hh ]",
"[ 11/4 → 23/8 | s:hh ]",
"[ 23/8 → 3/1 | s:oh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 25/8 → 13/4 | s:hh ]",
"[ 13/4 → 27/8 | s:hh ]",
"[ 27/8 → 7/2 | s:hh ]",
"[ 7/2 → 29/8 | s:hh ]",
"[ 29/8 → 15/4 | s:hh ]",
"[ 15/4 → 31/8 | s:hh ]",
"[ 31/8 → 4/1 | s:oh ]",
]
`;
exports[`runs examples > example "firstOf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:g3 ]",
@@ -5124,149 +4937,149 @@ exports[`runs examples > example "perlin" example index 0 1`] = `
exports[`runs examples > example "phaser" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:1 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:4 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:8 ]",
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:1 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:1 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:1 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:1 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:4 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:4 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:4 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:4 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:4 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:4 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:4 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:4 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:8 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:8 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:8 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:8 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:8 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:8 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:8 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:8 ]",
]
`;
exports[`runs examples > example "phasercenter" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:2000 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:4000 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasercenter:800 ]",
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
]
`;
exports[`runs examples > example "phaserdepth" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.5 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:0.75 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phaserdepth:1 ]",
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
]
`;
exports[`runs examples > example "phasersweep" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:2000 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:4000 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaserrate:2 phasersweep:800 ]",
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
]
`;
@@ -6976,27 +6789,6 @@ exports[`runs examples > example "segment" example index 0 1`] = `
`;
exports[`runs examples > example "seq" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:e5 ]",
"[ 1/3 → 2/3 | note:b4 ]",
"[ 2/3 → 5/6 | note:d5 ]",
"[ 5/6 → 1/1 | note:c5 ]",
"[ 1/1 → 4/3 | note:e5 ]",
"[ 4/3 → 5/3 | note:b4 ]",
"[ 5/3 → 11/6 | note:d5 ]",
"[ 11/6 → 2/1 | note:c5 ]",
"[ 2/1 → 7/3 | note:e5 ]",
"[ 7/3 → 8/3 | note:b4 ]",
"[ 8/3 → 17/6 | note:d5 ]",
"[ 17/6 → 3/1 | note:c5 ]",
"[ 3/1 → 10/3 | note:e5 ]",
"[ 10/3 → 11/3 | note:b4 ]",
"[ 11/3 → 23/6 | note:d5 ]",
"[ 23/6 → 4/1 | note:c5 ]",
]
`;
exports[`runs examples > example "seq" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 1/8 → 1/4 | s:hh ]",
@@ -7037,23 +6829,24 @@ exports[`runs examples > example "seq" example index 1 1`] = `
]
`;
exports[`runs examples > example "seqPLoop" example index 0 1`] = `
exports[`runs examples > example "seq" example index 0 2`] = `
[
"[ 0/1 → 1/8 | s:bd ]",
"[ 3/8 → 1/2 | s:bd ]",
"[ 3/4 → 7/8 | s:bd ]",
"[ 1/1 → 9/8 | s:bd ]",
"[ 1/1 → 9/8 | s:cp ]",
"[ 11/8 → 3/2 | s:bd ]",
"[ 11/8 → 3/2 | s:cp ]",
"[ 7/4 → 15/8 | s:bd ]",
"[ 7/4 → 15/8 | s:cp ]",
"[ 2/1 → 17/8 | s:cp ]",
"[ 19/8 → 5/2 | s:cp ]",
"[ 11/423/8 | s:cp ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 27/8 → 7/2 | s:bd ]",
"[ 15/4 → 31/8 | s:bd ]",
"[ 0/1 → 1/3 | note:e5 ]",
"[ 1/3 → 2/3 | note:b4 ]",
"[ 2/3 → 5/6 | note:d5 ]",
"[ 5/6 → 1/1 | note:c5 ]",
"[ 1/1 → 4/3 | note:e5 ]",
"[ 4/3 → 5/3 | note:b4 ]",
"[ 5/3 → 11/6 | note:d5 ]",
"[ 11/6 → 2/1 | note:c5 ]",
"[ 2/1 → 7/3 | note:e5 ]",
"[ 7/3 → 8/3 | note:b4 ]",
"[ 8/3 → 17/6 | note:d5 ]",
"[ 17/6 → 3/1 | note:c5 ]",
"[ 3/1 → 10/3 | note:e5 ]",
"[ 10/3 → 11/3 | note:b4 ]",
"[ 11/3 → 23/6 | note:d5 ]",
"[ 23/6 → 4/1 | note:c5 ]",
]
`;
@@ -7730,27 +7523,6 @@ exports[`runs examples > example "squiz" example index 0 1`] = `
`;
exports[`runs examples > example "stack" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:e4 ]",
"[ 0/1 → 1/1 | note:g3 ]",
"[ 0/1 → 1/1 | note:b3 ]",
"[ 1/2 → 1/1 | note:d4 ]",
"[ 1/1 → 3/2 | note:e4 ]",
"[ 1/1 → 2/1 | note:g3 ]",
"[ 1/1 → 2/1 | note:b3 ]",
"[ 3/2 → 2/1 | note:d4 ]",
"[ 2/1 → 5/2 | note:e4 ]",
"[ 2/1 → 3/1 | note:g3 ]",
"[ 2/1 → 3/1 | note:b3 ]",
"[ 5/2 → 3/1 | note:d4 ]",
"[ 3/1 → 7/2 | note:e4 ]",
"[ 3/1 → 4/1 | note:g3 ]",
"[ 3/1 → 4/1 | note:b3 ]",
"[ 7/2 → 4/1 | note:d4 ]",
]
`;
exports[`runs examples > example "stack" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:c4 ]",
"[ 0/1 → 1/4 | s:hh ]",
@@ -7791,6 +7563,27 @@ exports[`runs examples > example "stack" example index 1 1`] = `
]
`;
exports[`runs examples > example "stack" example index 0 2`] = `
[
"[ 0/1 → 1/2 | note:e4 ]",
"[ 0/1 → 1/1 | note:g3 ]",
"[ 0/1 → 1/1 | note:b3 ]",
"[ 1/2 → 1/1 | note:d4 ]",
"[ 1/1 → 3/2 | note:e4 ]",
"[ 1/1 → 2/1 | note:g3 ]",
"[ 1/1 → 2/1 | note:b3 ]",
"[ 3/2 → 2/1 | note:d4 ]",
"[ 2/1 → 5/2 | note:e4 ]",
"[ 2/1 → 3/1 | note:g3 ]",
"[ 2/1 → 3/1 | note:b3 ]",
"[ 5/2 → 3/1 | note:d4 ]",
"[ 3/1 → 7/2 | note:e4 ]",
"[ 3/1 → 4/1 | note:g3 ]",
"[ 3/1 → 4/1 | note:b3 ]",
"[ 7/2 → 4/1 | note:d4 ]",
]
`;
exports[`runs examples > example "steps" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
+5
View File
@@ -3,6 +3,11 @@ import { configDefaults, defineConfig } from 'vitest/config';
/// <reference types="vitest" />
export default defineConfig({
test: {
// ...
deps: {
registerNodeLoader: true,
},
threads: false,
reporters: 'verbose',
isolate: false,
silent: true,
+5
View File
@@ -0,0 +1,5 @@
{
"devToolbar": {
"enabled": false
}
}
+290
View File
@@ -0,0 +1,290 @@
declare module 'astro:content' {
interface Render {
'.mdx': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}>;
}
}
declare module 'astro:content' {
interface Render {
'.md': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}>;
}
}
declare module 'astro:content' {
export { z } from 'astro/zod';
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
// This needs to be in sync with ImageMetadata
export type ImageFunction = () => import('astro/zod').ZodObject<{
src: import('astro/zod').ZodString;
width: import('astro/zod').ZodNumber;
height: import('astro/zod').ZodNumber;
format: import('astro/zod').ZodUnion<
[
import('astro/zod').ZodLiteral<'png'>,
import('astro/zod').ZodLiteral<'jpg'>,
import('astro/zod').ZodLiteral<'jpeg'>,
import('astro/zod').ZodLiteral<'tiff'>,
import('astro/zod').ZodLiteral<'webp'>,
import('astro/zod').ZodLiteral<'gif'>,
import('astro/zod').ZodLiteral<'svg'>,
import('astro/zod').ZodLiteral<'avif'>,
]
>;
}>;
type BaseSchemaWithoutEffects =
| import('astro/zod').AnyZodObject
| import('astro/zod').ZodUnion<[BaseSchemaWithoutEffects, ...BaseSchemaWithoutEffects[]]>
| import('astro/zod').ZodDiscriminatedUnion<string, import('astro/zod').AnyZodObject[]>
| import('astro/zod').ZodIntersection<BaseSchemaWithoutEffects, BaseSchemaWithoutEffects>;
type BaseSchema =
| BaseSchemaWithoutEffects
| import('astro/zod').ZodEffects<BaseSchemaWithoutEffects>;
export type SchemaContext = { image: ImageFunction };
type DataCollectionConfig<S extends BaseSchema> = {
type: 'data';
schema?: S | ((context: SchemaContext) => S);
};
type ContentCollectionConfig<S extends BaseSchema> = {
type?: 'content';
schema?: S | ((context: SchemaContext) => S);
};
type CollectionConfig<S> = ContentCollectionConfig<S> | DataCollectionConfig<S>;
export function defineCollection<S extends BaseSchema>(
input: CollectionConfig<S>
): CollectionConfig<S>;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown
): Promise<CollectionEntry<C>[]>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(entry: {
collection: C;
slug: E;
}): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(entry: {
collection: C;
id: E;
}): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: {
collection: C;
slug: ValidContentEntrySlug<C>;
}[]
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: {
collection: C;
id: keyof DataEntryMap[C];
}[]
): Promise<CollectionEntry<C>[]>;
export function reference<C extends keyof AnyEntryMap>(
collection: C
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? {
collection: C;
slug: ValidContentEntrySlug<C>;
}
: {
collection: C;
id: keyof DataEntryMap[C];
}
>;
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
export function reference<C extends string>(
collection: C
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
"blog": {
"release-0.0.2-schwindlig.mdx": {
id: "release-0.0.2-schwindlig.mdx";
slug: "release-002-schwindlig";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.2.1-stuermisch.mdx": {
id: "release-0.0.2.1-stuermisch.mdx";
slug: "release-0021-stuermisch";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.3-maelstrom.mdx": {
id: "release-0.0.3-maelstrom.mdx";
slug: "release-003-maelstrom";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.4-gischt.mdx": {
id: "release-0.0.4-gischt.mdx";
slug: "release-004-gischt";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.3.0-donauwelle.mdx": {
id: "release-0.3.0-donauwelle.mdx";
slug: "release-030-donauwelle";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.4.0-brandung.mdx": {
id: "release-0.4.0-brandung.mdx";
slug: "release-040-brandung";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.5.0-wirbel.mdx": {
id: "release-0.5.0-wirbel.mdx";
slug: "release-050-wirbel";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.6.0-zimtschnecke.mdx": {
id: "release-0.6.0-zimtschnecke.mdx";
slug: "release-060-zimtschnecke";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.7.0-zuckerguss.mdx": {
id: "release-0.7.0-zuckerguss.mdx";
slug: "release-070-zuckerguss";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.8.0-himbeermuffin.mdx": {
id: "release-0.8.0-himbeermuffin.mdx";
slug: "release-080-himbeermuffin";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.9.0-bananenbrot.mdx": {
id: "release-0.9.0-bananenbrot.mdx";
slug: "release-090-bananenbrot";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-1.0.0-geburtstagskuchen.mdx": {
id: "release-1.0.0-geburtstagskuchen.mdx";
slug: "release-100-geburtstagskuchen";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"year-2.mdx": {
id: "year-2.mdx";
slug: "year-2";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
};
};
type DataEntryMap = {
};
type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ContentConfig = typeof import("../src/content/config");
}
-1
View File
@@ -1,4 +1,3 @@
.astro
# build output
dist/
-1
View File
@@ -35,7 +35,6 @@
"@strudel/mini": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/serial": "workspace:*",
"@strudel/shader": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
+2 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
import { Panel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
@@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div>
<UserFacingErrorMessage error={error} />
<HorizontalPanel context={context} />
<Panel context={context} />
</div>
);
}
@@ -26,7 +26,7 @@ author: froos
- fix: 💄 Enhance visualisation of the Tutorial on mobile by @puria in https://github.com/tidalcycles/strudel/pull/19
- Stateful queries and events (WIP) by @yaxu in https://github.com/tidalcycles/strudel/pull/14
- Fix resolveState by @yaxu in https://github.com/tidalcycles/strudel/pull/22
- added \_asNumber + interpret numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21
- added \_asNumber + interprete numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21
- Update package.json by @ChiakiUehira in https://github.com/tidalcycles/strudel/pull/23
- packaging by @felixroos in https://github.com/tidalcycles/strudel/pull/24
@@ -28,7 +28,7 @@ author: froos
- support freq in pianoroll by @felixroos in https://github.com/tidalcycles/strudel/pull/308
- ICLC2023 paper WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/306
- fix: copy share link to clipboard was broken for some browsers by @felixroos in https://github.com/tidalcycles/strudel/pull/311
- fix: copy share link to clipboard was broken for some browers by @felixroos in https://github.com/tidalcycles/strudel/pull/311
- Jsdoc component by @felixroos in https://github.com/tidalcycles/strudel/pull/312
- object support for .scale by @felixroos in https://github.com/tidalcycles/strudel/pull/307
- Astro build by @felixroos in https://github.com/tidalcycles/strudel/pull/315
@@ -138,7 +138,7 @@ Plenty of things have been added to the docs, including a [showcase of what peop
- Export patterns + ui tweaks by @felixroos in https://github.com/tidalcycles/strudel/pull/855
- Pattern organization by @felixroos in https://github.com/tidalcycles/strudel/pull/858
- Sound Import from local file system by @daslyfe in https://github.com/tidalcycles/strudel/pull/839
- bugfix: suspend and close existing audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882
- bugfix: suspend and close exisiting audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882
- add root mode for voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/887
- scales can now be anchored by @felixroos in https://github.com/tidalcycles/strudel/pull/888
- add dough function for raw dsp by @felixroos in https://github.com/tidalcycles/strudel/pull/707 (experimental)
@@ -195,7 +195,7 @@ Plenty of things have been added to the docs, including a [showcase of what peop
- Update tauri.yml workflow file by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/705
- vite-vanilla-repl readme fix by @felixroos in https://github.com/tidalcycles/strudel/pull/737
- completely revert config mess by @felixroos in https://github.com/tidalcycles/strudel/pull/745
- hopefully fix trailing slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753
- hopefully fix trainling slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753
- Update vite pwa by @felixroos in https://github.com/tidalcycles/strudel/pull/772
- Update to Astro 3 by @felixroos in https://github.com/tidalcycles/strudel/pull/775
- support multiple named serial connections, change default baudrate by @yaxu in https://github.com/tidalcycles/strudel/pull/551
+2 -5
View File
@@ -1,6 +1,7 @@
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { Icon } from './Icon';
import { silence, noteToMidi, _mod } from '@strudel/core';
import { clearHydra } from '@strudel/hydra';
import { getDrawContext, getPunchcardPainter } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
@@ -36,7 +37,7 @@ export function MiniRepl({
const shouldShowCanvas = !!punchcard;
const canvasId = shouldShowCanvas ? useMemo(() => `canvas-${id}`, [id]) : null;
autodraw = !!punchcard || !!claviature || !!autodraw;
drawTime = (drawTime ?? punchcard) ? [0, 4] : [-2, 2];
drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2];
if (claviature) {
drawTime = [0, 0];
}
@@ -119,7 +120,6 @@ export function MiniRepl({
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
aria-label={started ? 'stop' : 'play'}
onClick={() => editorRef.current?.toggle()}
>
<Icon type={started ? 'stop' : 'play'} />
@@ -129,7 +129,6 @@ export function MiniRepl({
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
aria-label="update"
onClick={() => editorRef.current?.evaluate()}
>
<Icon type="refresh" />
@@ -141,7 +140,6 @@ export function MiniRepl({
className={
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
}
aria-label="previous example"
onClick={() => changeTune(tuneIndex - 1)}
>
<div className="rotate-180">
@@ -152,7 +150,6 @@ export function MiniRepl({
className={
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
}
aria-label="next example"
onClick={() => changeTune(tuneIndex + 1)}
>
<Icon type="skip" />
+12
View File
@@ -25,14 +25,26 @@ These are the equivalents used by the Mini Notation:
<JsDoc client:idle name="cat" h={0} />
You can also use cat as a chained function like this:
<JsDoc client:idle name="Pattern.cat" h={0} hideDescription />
## seq
<JsDoc client:idle name="seq" h={0} />
Or as a chained function:
<JsDoc client:idle name="Pattern.seq" h={0} hideDescription />
## stack
<JsDoc client:idle name="stack" h={0} />
As a chained function:
<JsDoc client:idle name="Pattern.stack" h={0} hideDescription />
## s_cat
<JsDoc client:idle name="s_cat" h={0} />
+1 -1
View File
@@ -43,7 +43,7 @@ export function Header({ context, embedded = false }) {
)}
>
<div
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer', isZen && 'fixed top-2 right-4')}
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer')}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
+3 -3
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import { Panel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
@@ -21,10 +21,10 @@ export default function ReplEditor(Props) {
<Header context={context} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{panelPosition === 'right' && <VerticalPanel context={context} />}
{panelPosition === 'right' && <Panel context={context} />}
</div>
<UserFacingErrorMessage error={error} />
{panelPosition === 'bottom' && <HorizontalPanel context={context} />}
{panelPosition === 'bottom' && <Panel context={context} />}
</div>
);
}
@@ -2,8 +2,8 @@ import cx from '@src/cx.mjs';
export function ConsoleTab({ log }) {
return (
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm py-2">
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
+74 -133
View File
@@ -1,9 +1,10 @@
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { setPanelPinned, setActiveFooter as setTab, useSettings } from '../../../settings.mjs';
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
import { setActiveFooter, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
@@ -11,87 +12,33 @@ import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { ChevronLeftIcon } from '@heroicons/react/16/solid';
import useClient from '@src/useClient.mjs';
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function HorizontalPanel({ context }) {
const settings = useSettings();
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
className={cx(
'hover:max-h-[360px] hover:min-h-[360px] justify-between flex flex-col',
pinned ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10',
)}
>
<div className="flex h-full overflow-auto ">
<PanelContent context={context} tab={tab} />
</div>
<div className="flex justify-between min-h-10 max-h-10 pr-2 items-center">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPanelPinned} />
</div>
</PanelNav>
);
}
export function VerticalPanel({ context }) {
const settings = useSettings();
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
className={cx(
'hover:min-w-[min(600px,80vw)] hover:max-w-[min(600px,80vw)]',
pinned ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-8',
)}
>
<div className={cx('group-hover:flex flex-col h-full', pinned ? 'flex' : 'hidden')}>
<div className="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPanelPinned} />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
</div>
<div className={cx(pinned ? 'hidden' : 'flex flex-col items-center justify-center h-full group-hover:hidden ')}>
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
</div>
</PanelNav>
);
}
const tabNames = {
welcome: 'intro',
patterns: 'patterns',
sounds: 'sounds',
reference: 'reference',
console: 'console',
settings: 'settings',
};
if (TAURI) {
tabNames.files = 'files';
}
function PanelNav({ children, className, ...props }) {
return (
<nav
aria-label="Settings Menu"
className={cx('bg-lineHighlight group transition-all overflow-x-auto', className)}
{...props}
>
{children}
</nav>
);
}
function PanelContent({ context, tab }) {
export function Panel({ context }) {
const footerContent = useRef();
const [log, setLog] = useState([]);
const { activeFooter, isZen, panelPosition } = useSettings();
useIsomorphicLayoutEffect(() => {
if (footerContent.current && activeFooter === 'console') {
// scroll log box to bottom when log changes
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
}
}, [log, activeFooter]);
useIsomorphicLayoutEffect(() => {
if (!footerContent.current) {
} else if (activeFooter === 'console') {
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
} else {
footerContent.current.scrollTop = 0;
}
}, [activeFooter]);
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
@@ -113,72 +60,66 @@ function PanelContent({ context, tab }) {
}, []),
);
switch (tab) {
case tabNames.patterns:
return <PatternsTab context={context} />;
case tabNames.console:
return <ConsoleTab log={log} />;
case tabNames.sounds:
return <SoundsTab />;
case tabNames.reference:
return <Reference />;
case tabNames.settings:
return <SettingsTab started={context.started} />;
case tabNames.files:
return <FilesTab />;
default:
return <WelcomeTab context={context} />;
}
}
function PanelTab({ label, isSelected, onClick }) {
return (
const PanelTab = ({ children, name, label }) => (
<>
<div
onClick={onClick}
onClick={() => setActiveFooter(name)}
className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
isSelected ? 'border-foreground' : 'border-transparent',
activeFooter === name ? 'border-foreground' : 'border-transparent',
)}
>
{label}
{label || name}
</div>
{activeFooter === name && <>{children}</>}
</>
);
}
function Tabs({ setTab, tab }) {
return (
<div className={cx('flex select-none max-w-full overflow-auto pb-2')}>
{Object.keys(tabNames).map((key) => {
const val = tabNames[key];
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
})}
</div>
);
}
const client = useClient();
if (isZen) {
return null;
}
function PinButton({ pinned, setPinned }) {
const isActive = activeFooter !== '';
let positions = {
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
};
if (!client) {
return null;
}
return (
<button
onClick={() => setPinned(!pinned)}
className={cx(
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
pinned ? 'flex' : 'hidden',
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}>
<div className="flex justify-between px-2">
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
<PanelTab name="intro" label="welcome" />
<PanelTab name="patterns" />
<PanelTab name="sounds" />
<PanelTab name="console" />
<PanelTab name="reference" />
<PanelTab name="settings" />
{TAURI && <PanelTab name="files" />}
</div>
{activeFooter !== '' && (
<button onClick={() => setActiveFooter('')} className="text-foreground px-2" aria-label="Close Panel">
<XMarkIcon className="w-5 h-5" />
</button>
)}
</div>
{activeFooter !== '' && (
<div className="relative overflow-hidden">
<div className="text-white overflow-auto h-full max-w-full" ref={footerContent}>
{activeFooter === 'intro' && <WelcomeTab context={context} />}
{activeFooter === 'patterns' && <PatternsTab context={context} />}
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab started={context.started} />}
{activeFooter === 'files' && <FilesTab />}
</div>
</div>
)}
aria-label="Pin Settings Menu"
>
<svg
stroke="currentColor"
fill={'currentColor'}
strokeWidth="0"
className="w-full h-full"
opacity={pinned ? 1 : '.3'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path>
</svg>
</button>
</nav>
);
}
@@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
function ActionButton({ children, onClick, label, labelIsHidden }) {
return (
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
<button className="hover:opacity-50" onClick={onClick} title={label}>
{labelIsHidden !== true && label}
{children}
</button>
@@ -102,7 +102,7 @@ export function PatternsTab({ context }) {
const autoResetPatternOnChange = !isUdels();
return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
@@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
</div>
)}
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
<section className="flex overflow-y-scroll max-h-full flex-col">
{patternFilter === patternFilterName.user && (
<PatternButtons
onClick={(id) =>
+18 -45
View File
@@ -1,7 +1,5 @@
import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
const availableFunctions = jsdocJson.docs
const visibleFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
@@ -12,50 +10,25 @@ const getInnerText = (html) => {
};
export function Reference() {
const [search, setSearch] = useState('');
const visibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (!search) {
return true;
}
return entry.name.includes(search) || (entry.synonyms?.some((s) => s.includes(search)) ?? false);
});
}, [search]);
return (
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div class="w-full flex">
<input
className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
</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 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);
}}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
))}
</div>
<div className="flex h-full w-full pt-2 text-foreground overflow-hidden">
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer block hover:bg-lineHighlight py-1 px-4"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
))}
</div>
<div
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
id="reference-container"
>
<div className="prose dark:prose-invert min-w-full px-1 ">
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative" id="reference-container">
<div className="prose dark:prose-invert max-w-full pr-4">
<h2>API Reference</h2>
<p>
This is the long list functions you can use! Remember that you don't need to remember all of those and that
@@ -1,6 +1,7 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { useMemo } from 'react';
import { audioEngineTargets, defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror';
import { isUdels } from '../../util.mjs';
import { isUdels, setGlobalAudioVolume } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
@@ -31,23 +32,47 @@ function SelectInput({ value, options, onChange }) {
);
}
function NumberSlider({ value, onChange, step = 1, ...rest }) {
function NumberSlider({ value, onChange, min, max, step = 1, ...rest }) {
const fractionalDigits = useMemo(() => {
const stepStr = step.toString();
const decimalPointIdx = stepStr.indexOf('.');
if (decimalPointIdx < 0) {
return 0;
}
return stepStr.slice(decimalPointIdx + 1).length;
}, [step]);
const textInputCharWidth = useMemo(() => {
const maxValueWholePartLength = Math.floor(max).toString().length;
return maxValueWholePartLength + '.'.length + fractionalDigits;
}, [max, fractionalDigits]);
return (
<div className="flex space-x-2 gap-1">
<input
className="p-2 grow"
type="range"
value={value}
min={min}
max={max}
step={step}
onChange={(e) => onChange(Number(e.target.value))}
{...rest}
/>
<input
type="number"
value={value}
style={{
// approximate text size + some leeway for the default padding + some space between the browser's up/down arrows and the input's value
width: `calc(${textInputCharWidth}ch + 2 * 0.75rem + 1rem)`,
}}
value={Number(value).toFixed(fractionalDigits)}
min={min}
max={max}
step={step}
className="w-16 bg-background rounded-md"
className="bg-background rounded-md"
onChange={(e) => onChange(Number(e.target.value))}
{...rest}
/>
</div>
);
@@ -101,11 +126,12 @@ export function SettingsTab({ started }) {
panelPosition,
audioDeviceName,
audioEngineTarget,
audioVolume,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return (
<div className="text-foreground p-4 space-y-4 w-full">
<div className="text-foreground p-4 space-y-4">
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@@ -135,6 +161,19 @@ export function SettingsTab({ started }) {
}}
/>
</FormItem>
<FormItem label="Audio Volume">
{audioEngineTarget === audioEngineTargets.osc && (
<span class="text-sm italic">Has no effect when Audio Engine Target is OSC</span>
)}
<NumberSlider
value={audioVolume}
onChange={(audioVolume) => settingsMap.setKey('audioVolume', audioVolume)}
min={0}
max={100}
step={0.1}
disabled={audioEngineTarget === audioEngineTargets.osc}
/>
</FormItem>
<FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem>
@@ -1,7 +1,7 @@
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 React, { useMemo, useRef } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
@@ -12,20 +12,15 @@ const getSamples = (samples) =>
export function SoundsTab() {
const sounds = useStore(soundMap);
const { soundsFilter } = useSettings();
const [search, setSearch] = useState('');
const soundEntries = useMemo(() => {
let filtered = Object.entries(sounds)
.filter(([key]) => !key.startsWith('_'))
.sort((a, b) => a[0].localeCompare(b[0]));
if (!sounds) {
return [];
}
let filtered = Object.entries(sounds)
.filter(([key]) => !key.startsWith('_'))
.sort((a, b) => a[0].localeCompare(b[0]))
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
if (soundsFilter === 'user') {
return filtered.filter(([_, { data }]) => !data.prebake);
return filtered.filter(([key, { data }]) => !data.prebake);
}
if (soundsFilter === 'drums') {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
@@ -37,11 +32,9 @@ export function SoundsTab() {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
return filtered;
}, [sounds, soundsFilter, search]);
}, [sounds, soundsFilter]);
// holds mutable ref to current triggered sound
const trigRef = useRef();
// stop current sound on mouseup
useEvent('mouseup', () => {
const t = trigRef.current;
@@ -50,16 +43,8 @@ export function SoundsTab() {
ref?.stop(getAudioContext().currentTime + 0.01);
});
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<input
className="w-full p-1 bg-background rounded-md pb-2"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup
value={soundsFilter}
@@ -73,8 +58,7 @@ export function SoundsTab() {
></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -5,7 +5,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) {
return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
<h3>
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
</h3>
+3 -3
View File
@@ -23,10 +23,10 @@ async function hasStrudelJson(subpath) {
async function loadStrudelJson(subpath) {
const contents = await readTextFile(subpath + '/strudel.json', { dir });
const sampleMap = JSON.parse(contents);
processSampleMap(sampleMap, (key, bank) => {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank, fileResolver(subpath)), {
processSampleMap(sampleMap, (key, value) => {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value, fileResolver(subpath)), {
type: 'sample',
samples: bank,
samples: value,
fileSystem: true,
tag: 'local',
});
+5 -1
View File
@@ -14,7 +14,7 @@ import {
resetLoadedSounds,
initAudioOnFirstClick,
} from '@strudel/webaudio';
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
import { getAudioDevices, setAudioDevice, setGlobalAudioVolume, setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { clearHydra } from '@strudel/hydra';
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -162,6 +162,7 @@ export function useReplContext() {
// on first load, set stored audio device if possible
useEffect(() => {
const { audioDeviceName } = _settings;
if (audioDeviceName !== defaultAudioDeviceName) {
getAudioDevices().then((devices) => {
const deviceID = devices.get(audioDeviceName);
@@ -173,6 +174,9 @@ export function useReplContext() {
}
}, []);
// set stored audio volume
useEffect(() => setGlobalAudioVolume(_settings.audioVolume), [_settings.audioVolume]);
//
// UI Actions
//
+20 -2
View File
@@ -1,6 +1,12 @@
import { evalScope, hash2code, logger } from '@strudel/core';
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio';
import {
getAudioContext,
initializeAudioOutput,
setDefaultAudioContext,
setGlobalGain,
setVersionDefaults,
} from '@strudel/webaudio';
import { getMetadata } from '../metadata_parser';
import { isTauri } from '../tauri.mjs';
import './Repl.css';
@@ -78,7 +84,6 @@ export function loadModules() {
import('@strudel/codemirror'),
import('@strudel/hydra'),
import('@strudel/serial'),
import('@strudel/shader'),
import('@strudel/soundfonts'),
import('@strudel/csound'),
import('@strudel/tidal'),
@@ -189,6 +194,19 @@ export const setAudioDevice = async (id) => {
initializeAudioOutput();
};
const NATURAL_LOG_10 = Math.log(1000);
const LOW_VOLUME_CONSTANT = Math.exp(0.1 * NATURAL_LOG_10);
export const setGlobalAudioVolume = (volume) => {
// Gain is calculated to adjust the volume to a logarithmic scale to match how us humans perceive loudness.
// Formula is taken from https://www.dr-lex.be/info-stuff/volumecontrols.html
volume /= 100; // [0, 1]
let gain = volume >= 0.1 ? 0.001 * Math.exp(NATURAL_LOG_10 * volume) : volume * 10 * 0.001 * LOW_VOLUME_CONSTANT;
gain = Math.max(0, Math.min(gain, 1)); // just in case
setGlobalGain(gain);
};
export function setVersionDefaultsFrom(code) {
try {
const metadata = getMetadata(code);
+2 -4
View File
@@ -30,11 +30,11 @@ export const defaultSettings = {
isZen: false,
soundsFilter: 'all',
patternFilter: 'community',
panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom',
isPanelPinned: true,
panelPosition: 'right',
userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio,
audioVolume: 50,
};
let search = null;
@@ -61,7 +61,6 @@ export function useSettings() {
return {
...state,
isZen: parseBoolean(state.isZen),
isPanelPinned: parseBoolean(state.isPanelPinned),
isBracketMatchingEnabled: parseBoolean(state.isBracketMatchingEnabled),
isBracketClosingEnabled: parseBoolean(state.isBracketClosingEnabled),
isLineNumbersDisplayed: parseBoolean(state.isLineNumbersDisplayed),
@@ -79,7 +78,6 @@ export function useSettings() {
}
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (isPinned) => settingsMap.setKey('isPanelPinned', isPinned);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
-1
View File
@@ -52,7 +52,6 @@
}
:root {
--app-height: 100vh;
--app-width: 100vw;
}
#console-tab {
-1
View File
@@ -34,7 +34,6 @@ module.exports = {
},
spacing: {
'app-height': 'var(--app-height)',
'app-width': 'var(--app-width)',
},
typography(theme) {
return {