mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc335c9930 | |||
| 0d67176a98 | |||
| 77bbb9626a | |||
| 65cc317382 | |||
| 36d4d89052 | |||
| f8a241401c | |||
| 1baa6da370 | |||
| 9e0b840afd | |||
| e6fbef95f2 | |||
| d9bd6f9e7d | |||
| 1e0cc2ab17 | |||
| cad11e1e48 | |||
| b85a5a9179 | |||
| 9b4675f1ce | |||
| 67c0ebaf54 | |||
| 8d1180f87a | |||
| fcd550b5bd | |||
| d3e624f673 | |||
| b0149f8190 | |||
| bf3a095852 | |||
| 5c983ceb8f | |||
| 735d531cce | |||
| 835c7b6879 | |||
| 13168bb9bc | |||
| 46b4aff0ef | |||
| 721e7f06b8 | |||
| a46bbeaa10 | |||
| e3a3c0c5f9 | |||
| 67964c7e23 | |||
| 1b8998ac14 | |||
| 9e5f9d37ef | |||
| 0c1234a35f | |||
| c6cc487fc6 | |||
| f0ddb2d595 | |||
| dd171f8320 | |||
| c22c786cf2 | |||
| c191471e1e | |||
| 0d0cb3fd93 | |||
| bb4b7dc533 | |||
| 8440fa42ea | |||
| 61f070744a | |||
| 2e6a561184 | |||
| 8703d684c4 | |||
| b0d2115f43 | |||
| 96802a271c | |||
| a91f4576f3 | |||
| ad080d02b3 | |||
| e9e7e02547 | |||
| 2ffc6c9601 | |||
| 3a9349d9f5 | |||
| bd3f2845f8 | |||
| 2d320c459b | |||
| ff9f4a8a0d | |||
| d7b94807cb | |||
| 2c16a37a68 | |||
| 717e4a2c1d | |||
| c225b4719f | |||
| 25f762c393 | |||
| b75561e3c7 | |||
| abaab89061 | |||
| e409086b34 | |||
| 532f1435d9 | |||
| 6cb61ade03 | |||
| a40219ba06 | |||
| 9c99f5ada2 | |||
| 2d72c8faa0 | |||
| b188fc6b6d | |||
| bfb22228cc | |||
| 9714d88d61 | |||
| bdddb91510 | |||
| 07bcaa65a9 | |||
| 5bf1805534 | |||
| 202685e8c4 | |||
| aba633e16e | |||
| da45ce689c | |||
| acea7e435e | |||
| cb4810cbbc | |||
| 5ec205b383 | |||
| ebc0b293a9 | |||
| 4608c321bd | |||
| c4243f41c6 | |||
| d95b8bc667 | |||
| 263c13c36e | |||
| ddceb36613 | |||
| ff8ec628be | |||
| 11a07df120 | |||
| 8befa4382b | |||
| 39006e4d71 | |||
| 812ed6b60d | |||
| 8b6c8eb35d | |||
| dcda9a5753 | |||
| d3733d4209 | |||
| ef6af96947 | |||
| d86ff0a536 | |||
| 2c9ea03fde | |||
| 4eae366bee | |||
| 117f7fa4b5 | |||
| 73ee84a1fb | |||
| 9c899d5308 | |||
| b60beaf508 | |||
| 796f741a92 | |||
| 8da1d9fbf3 | |||
| 5345986b5b | |||
| 3945abd7e9 | |||
| 053e4ee54b | |||
| 25d61a2e31 | |||
| 5e6131af24 | |||
| 2899ee5aef | |||
| 9d1674b13c | |||
| bfead58212 | |||
| 926c893c4f | |||
| 3cd31e4841 | |||
| c2be2ed76a | |||
| ac0dc7a8de | |||
| cba9169b2f | |||
| e9a0a06b77 | |||
| d80c06cd55 | |||
| d850726910 | |||
| 1b01f41ffd | |||
| ad00fa65f8 | |||
| 814babbf7a | |||
| 28334ec94d | |||
| 7d2110d75f | |||
| 94ccfa631e | |||
| c7c56a1357 | |||
| d40e4dac2f | |||
| 686247f0cb | |||
| dffeac41af | |||
| 6e496b7a4a | |||
| 71e843e1a3 | |||
| 61ba33043e | |||
| 943d78f268 | |||
| 5ac59e95dd | |||
| 03bbc0bc27 | |||
| 4c3457438d | |||
| 94c07b6b7a | |||
| 42000a7628 | |||
| b5fbcd108f | |||
| 5e9da78e64 | |||
| f3b83b41f5 | |||
| 1d95da7a3a | |||
| 2a2ddf205a | |||
| 6c9e0aaa1a | |||
| d23038d386 | |||
| 248f67d1ab | |||
| f3fa1483d7 | |||
| 11102adb75 | |||
| 9f958abb08 | |||
| 065b24a2ee | |||
| e6f3b1f1ef | |||
| 69a5d62027 | |||
| 226cf356d6 | |||
| 73d49f152a | |||
| 5ca9afa9e8 | |||
| cdba9efb59 |
@@ -23,3 +23,4 @@ reverbGen.mjs
|
||||
hydra.mjs
|
||||
jsdoc-synonyms.js
|
||||
packages/hs2js/src/hs2js.mjs
|
||||
samples
|
||||
@@ -29,6 +29,7 @@ const editor = new StrudelMirror({
|
||||
import('@strudel/core'),
|
||||
import('@strudel/draw'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/shader'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/webaudio'),
|
||||
);
|
||||
|
||||
+7
-7
@@ -56,18 +56,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@vitest/ui": "^1.1.0",
|
||||
"acorn": "^8.11.3",
|
||||
"@vitest/ui": "^2.1.3",
|
||||
"acorn": "^8.13.0",
|
||||
"dependency-tree": "^10.0.9",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"events": "^3.3.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsdoc": "^4.0.3",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"lerna": "^8.0.1",
|
||||
"prettier": "^3.1.1",
|
||||
"lerna": "^8.1.8",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +99,8 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
},
|
||||
{
|
||||
key: 'Alt-.',
|
||||
run: (_, e) => {
|
||||
e.preventDefault();
|
||||
onStop?.();
|
||||
},
|
||||
preventDefault: true,
|
||||
run: () => onStop?.(),
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
// import createClock from './zyklus.mjs';
|
||||
|
||||
function getTime() {
|
||||
const precision = 10 ** 4;
|
||||
const seconds = performance.now() / 1000;
|
||||
return Math.round(seconds * precision) / precision;
|
||||
const seconds = performance.now() * 0.001;
|
||||
return seconds;
|
||||
// return Math.round(seconds * precision) / precision;
|
||||
}
|
||||
|
||||
let num_cycles_at_cps_change = 0;
|
||||
@@ -24,27 +24,20 @@ const sendMessage = (type, payload) => {
|
||||
|
||||
const sendTick = (phase, duration, tick, time) => {
|
||||
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
|
||||
|
||||
const tickdeadline = phase - time;
|
||||
const lastTick = time + tickdeadline;
|
||||
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
|
||||
|
||||
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||
const secondsSinceLastTick = time - lastTick - duration;
|
||||
|
||||
const eventLength = duration * cps;
|
||||
const end = begin + eventLength;
|
||||
|
||||
const cycle = begin + secondsSinceLastTick * cps;
|
||||
|
||||
sendMessage('tick', {
|
||||
begin,
|
||||
end,
|
||||
cps,
|
||||
tickdeadline,
|
||||
num_cycles_at_cps_change,
|
||||
num_seconds_at_cps_change,
|
||||
num_seconds_since_cps_change,
|
||||
time,
|
||||
cycle,
|
||||
});
|
||||
num_ticks_since_cps_change++;
|
||||
|
||||
@@ -454,9 +454,6 @@ 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.
|
||||
*
|
||||
@@ -468,7 +465,11 @@ export const { phaserrate, phasr } = registerControl('phaserrate', 'phasr');
|
||||
* .phaser("<1 2 4 8>")
|
||||
*
|
||||
*/
|
||||
export const { phaser, ph } = registerControl(['phaser', 'phaserdepth', 'phasercenter', 'phasersweep'], 'ph');
|
||||
export const { phaserrate, ph, phaser } = registerControl(
|
||||
['phaserrate', 'phaserdepth', 'phasercenter', 'phasersweep'],
|
||||
'ph',
|
||||
'phaser',
|
||||
);
|
||||
|
||||
/**
|
||||
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
|
||||
@@ -1383,6 +1384,17 @@ export const { compressorRelease } = registerControl('compressorRelease');
|
||||
*
|
||||
*/
|
||||
export const { speed } = registerControl('speed');
|
||||
|
||||
/**
|
||||
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
|
||||
*
|
||||
* @name stretch
|
||||
* @param {number | Pattern} factor -inf to inf, negative numbers play the sample backwards.
|
||||
* @example
|
||||
* s("gm_flute").stretch("1 2 .5")
|
||||
*
|
||||
*/
|
||||
export const { stretch } = registerControl('stretch');
|
||||
/**
|
||||
* Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.
|
||||
*
|
||||
@@ -1393,6 +1405,7 @@ export const { speed } = registerControl('speed');
|
||||
* @superdirtOnly
|
||||
*
|
||||
*/
|
||||
|
||||
export const { unit } = registerControl('unit');
|
||||
/**
|
||||
* Made by Calum Gunn. Reminiscent of some weird mixture of filter, ring-modulator and pitch-shifter. The SuperCollider manual defines Squiz as:
|
||||
@@ -1525,7 +1538,7 @@ export const { cps } = registerControl('cps');
|
||||
* note("c a f e").s("piano").clip("<.5 1 2>")
|
||||
*
|
||||
*/
|
||||
export const { clip } = registerControl('clip');
|
||||
export const { clip, legato } = registerControl('clip', 'legato');
|
||||
|
||||
/**
|
||||
* Sets the duration of the event in cycles. Similar to clip / legato, it also cuts samples off at the end if they exceed the duration.
|
||||
|
||||
@@ -4,8 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { isPattern } from './index.mjs';
|
||||
|
||||
export const evalScope = async (...args) => {
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
@@ -39,6 +37,7 @@ function safeEval(str, options = {}) {
|
||||
|
||||
export const evaluate = async (code, transpiler, transpilerOptions) => {
|
||||
let meta = {};
|
||||
|
||||
if (transpiler) {
|
||||
// transform syntactically correct js code to semantically usable code
|
||||
const transpiled = transpiler(code, transpilerOptions);
|
||||
|
||||
@@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { logger } from './logger.mjs';
|
||||
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
||||
|
||||
export class NeoCyclist {
|
||||
constructor({ onTrigger, onToggle, getTime }) {
|
||||
@@ -13,79 +14,38 @@ export class NeoCyclist {
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.time_at_last_tick_message = 0;
|
||||
|
||||
this.num_cycles_at_cps_change = 0;
|
||||
// the clock of the worker and the audio context clock can drift apart over time
|
||||
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
||||
// we need to keep a rolling average of the time difference between the worker clock and audio context clock
|
||||
// in order to schedule events consistently.
|
||||
this.collator = new ClockCollator({ getTargetClockTime: getTime });
|
||||
this.onToggle = onToggle;
|
||||
this.latency = 0.1; // fixed trigger time offset
|
||||
this.cycle = 0;
|
||||
this.id = Math.round(Date.now() * Math.random());
|
||||
this.worker_time_dif;
|
||||
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
||||
this.worker.port.start();
|
||||
|
||||
this.channel = new BroadcastChannel('strudeltick');
|
||||
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
|
||||
const maxWeight = 20;
|
||||
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
|
||||
|
||||
// the clock of the worker and the audio context clock can drift apart over time
|
||||
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
||||
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
|
||||
// in order to schedule events consistently.
|
||||
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
|
||||
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
|
||||
if (this.worker_time_dif == null) {
|
||||
this.worker_time_dif = time_dif;
|
||||
} else {
|
||||
const w = 1; //weight of new time diff;
|
||||
const new_dif =
|
||||
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
|
||||
|
||||
if (new_dif != this.worker_time_dif) {
|
||||
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
|
||||
weight = 4;
|
||||
}
|
||||
this.worker_time_dif = new_dif;
|
||||
}
|
||||
weight = Math.min(weight + 1, maxWeight);
|
||||
};
|
||||
|
||||
const tickCallback = (payload) => {
|
||||
const {
|
||||
num_cycles_at_cps_change,
|
||||
cps,
|
||||
num_seconds_at_cps_change,
|
||||
num_seconds_since_cps_change,
|
||||
begin,
|
||||
end,
|
||||
tickdeadline,
|
||||
cycle,
|
||||
} = payload;
|
||||
const { cps, begin, end, cycle, time } = payload;
|
||||
this.cps = cps;
|
||||
this.cycle = cycle;
|
||||
|
||||
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
|
||||
|
||||
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
|
||||
|
||||
this.time_at_last_tick_message = this.getTime();
|
||||
const currentTime = this.collator.calculateOffset(time) + time;
|
||||
processHaps(begin, end, currentTime);
|
||||
this.time_at_last_tick_message = currentTime;
|
||||
};
|
||||
|
||||
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
|
||||
const processHaps = (begin, end, currentTime) => {
|
||||
if (this.started === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
|
||||
haps.forEach((hap) => {
|
||||
if (hap.hasOnset()) {
|
||||
const targetTime =
|
||||
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
|
||||
seconds_at_cps_change +
|
||||
this.latency +
|
||||
this.worker_time_dif;
|
||||
const duration = hap.duration / this.cps;
|
||||
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
|
||||
const targetTime = timeUntilTrigger + currentTime + this.latency;
|
||||
const duration = cycleToSeconds(hap.duration, this.cps);
|
||||
onTrigger?.(hap, 0, duration, this.cps, targetTime);
|
||||
}
|
||||
});
|
||||
@@ -129,8 +89,8 @@ export class NeoCyclist {
|
||||
this.setStarted(true);
|
||||
}
|
||||
stop() {
|
||||
this.worker_time_dif = null;
|
||||
logger('[cyclist] stop');
|
||||
this.collator.reset();
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
+144
-110
@@ -386,6 +386,15 @@ export class Pattern {
|
||||
return this.fmap(func).squeezeJoin();
|
||||
}
|
||||
|
||||
polyJoin = function () {
|
||||
const pp = this;
|
||||
return pp.fmap((p) => p.s_extend(pp.tactus.div(p.tactus))).outerJoin();
|
||||
};
|
||||
|
||||
polyBind(func) {
|
||||
return this.fmap(func).polyJoin();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Utility methods mainly for internal use
|
||||
|
||||
@@ -754,6 +763,10 @@ export class Pattern {
|
||||
const otherPat = reify(other);
|
||||
return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).restartJoin();
|
||||
}
|
||||
_opPoly(other, func) {
|
||||
const otherPat = reify(other);
|
||||
return this.fmap((b) => otherPat.fmap((a) => func(a)(b))).polyJoin();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// End-user methods.
|
||||
@@ -796,15 +809,6 @@ 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);
|
||||
}
|
||||
@@ -813,30 +817,9 @@ 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);
|
||||
}
|
||||
@@ -1062,7 +1045,7 @@ function _composeOp(a, b, func) {
|
||||
func: [(a, b) => b(a)],
|
||||
};
|
||||
|
||||
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart'];
|
||||
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
|
||||
|
||||
// generate methods to do what and how
|
||||
for (const [what, [op, preprocess]] of Object.entries(composers)) {
|
||||
@@ -1267,6 +1250,12 @@ 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..
|
||||
@@ -1395,6 +1384,11 @@ 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);
|
||||
@@ -1417,12 +1411,45 @@ 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) {
|
||||
result = result._fast(pats.length);
|
||||
result.tactus = pats.length;
|
||||
}
|
||||
if (pats.length == 1 && pats[0].__tactus_source) {
|
||||
pats.tactus = pats[0].tactus;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1432,12 +1459,18 @@ export function sequence(...pats) {
|
||||
}
|
||||
|
||||
/** Like **cat**, but the items are crammed into one cycle.
|
||||
* @synonyms fastcat, sequence
|
||||
* @synonyms sequence, fastcat
|
||||
* @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);
|
||||
}
|
||||
@@ -1566,7 +1599,11 @@ export function register(name, func, patternify = true, preserveTactus = false,
|
||||
// There are patternified args, so lets make an unpatternified
|
||||
// version, prefixed by '_'
|
||||
Pattern.prototype['_' + name] = function (...args) {
|
||||
return func(...args, this);
|
||||
const result = func(...args, this);
|
||||
if (preserveTactus) {
|
||||
result.setTactus(this.tactus);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1729,75 +1766,6 @@ export const { compressSpan, compressspan } = register(['compressSpan', 'compres
|
||||
return pat._compress(span.begin, span.end);
|
||||
});
|
||||
|
||||
/**
|
||||
* fills in the gap between consecutive notes
|
||||
*
|
||||
* @name fill
|
||||
* @memberof Pattern
|
||||
* @param {Pattern | number | array} fill relative note length
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("sawtooth").euclid(11,16).fill("<1 .5>")._pianoroll()
|
||||
* @example
|
||||
* // second array parameter is "lookahead" which is the number of cycles in the future to query for the next event
|
||||
* s("supersaw").euclid(7,16).fill("1:<1 .15>")._pianoroll()
|
||||
*/
|
||||
export const fill = register('fill', function (legato, pat) {
|
||||
let multiplier = legato;
|
||||
let lookahead = 1;
|
||||
if (Array.isArray(legato)) {
|
||||
multiplier = legato[0] ?? 1;
|
||||
lookahead = legato[1] ?? lookahead;
|
||||
}
|
||||
|
||||
let spanEnd;
|
||||
let spanBegin;
|
||||
return pat
|
||||
.withQuerySpan((span) => {
|
||||
spanEnd = span.end;
|
||||
spanBegin = span.begin;
|
||||
|
||||
return span.withEnd((e) => {
|
||||
return e.add(lookahead);
|
||||
})
|
||||
// .withBegin(e => e.sub(lookahead));
|
||||
})
|
||||
.withHaps((haps) => {
|
||||
const newHaps = [];
|
||||
haps
|
||||
.sort((a, b) => {
|
||||
return a.part.begin.sub(b.part.begin).valueOf();
|
||||
})
|
||||
.forEach((hap, i) => {
|
||||
const nextHap = haps[i + 1];
|
||||
const partbegin = hap.part.begin;
|
||||
let partend = hap.part.end;
|
||||
const wholebegin = hap.whole.begin;
|
||||
// filter out haps that were not part of the original span
|
||||
if (wholebegin.gte(spanEnd) || wholebegin.lt(spanBegin)) {
|
||||
return;
|
||||
}
|
||||
let wholeend = hap.whole.end;
|
||||
if (nextHap != null) {
|
||||
partend = nextHap.part.begin;
|
||||
wholeend = nextHap.whole.begin;
|
||||
}
|
||||
|
||||
let wholeduration = wholeend.sub(wholebegin).mul(multiplier);
|
||||
let partduration = partend.sub(partbegin).mul(multiplier);
|
||||
|
||||
partend = partbegin.add(partduration);
|
||||
wholeend = wholebegin.add(wholeduration);
|
||||
|
||||
const newPart = new TimeSpan(partbegin, partend);
|
||||
const newWhole = new TimeSpan(wholebegin, wholeend);
|
||||
|
||||
newHaps.push(new Hap(newWhole, newPart, hap.value, hap.context));
|
||||
});
|
||||
return newHaps;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* speeds up a pattern like fast, but rather than it playing multiple times as fast would it instead leaves a gap in the remaining space of the cycle. For example, the following will play the sound pattern "bd sn" only once but compressed into the first half of the cycle, i.e. twice as fast.
|
||||
* @name fastGap
|
||||
@@ -2065,6 +2033,34 @@ 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
|
||||
@@ -2496,6 +2492,37 @@ 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
|
||||
@@ -2681,8 +2708,7 @@ export function s_cat(...timepats) {
|
||||
}
|
||||
if (timepats.length == 1) {
|
||||
const result = reify(timepats[0][1]);
|
||||
result.tactus = timepats[0][0];
|
||||
return result;
|
||||
return result.withTactus((_) => timepats[0][0]);
|
||||
}
|
||||
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
@@ -2775,6 +2801,10 @@ export const s_sub = stepRegister('s_sub', function (i, pat) {
|
||||
return pat.s_add(pat.tactus.sub(i));
|
||||
});
|
||||
|
||||
export const s_cycles = stepRegister('s_extend', function (factor, pat) {
|
||||
return pat.fast(factor).s_expand(factor);
|
||||
});
|
||||
|
||||
export const s_expand = stepRegister('s_expand', function (factor, pat) {
|
||||
return pat.withTactus((t) => t.mul(Fraction(factor)));
|
||||
});
|
||||
@@ -2988,7 +3018,8 @@ export const splice = register(
|
||||
);
|
||||
|
||||
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state));
|
||||
const tactus = pat.tactus ? pat.tactus.div(factor) : undefined;
|
||||
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), tactus);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -3002,11 +3033,14 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
|
||||
export const fit = register('fit', (pat) =>
|
||||
pat.withHaps((haps, state) =>
|
||||
haps.map((hap) =>
|
||||
hap.withValue((v) => ({
|
||||
...v,
|
||||
speed: (state.controls._cps || 1) / hap.whole.duration,
|
||||
unit: 'c',
|
||||
})),
|
||||
hap.withValue((v) => {
|
||||
const slicedur = ('end' in v ? v.end : 1) - ('begin' in v ? v.begin : 0);
|
||||
return {
|
||||
...v,
|
||||
speed: ((state.controls._cps || 1) / hap.whole.duration) * slicedur,
|
||||
unit: 'c',
|
||||
};
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -138,7 +138,7 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
||||
const timeToRandsPrime = (seed, n) => {
|
||||
const result = [];
|
||||
// eslint-disable-next-line
|
||||
for (let i = 0; i < n; ++n) {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
result.push(intSeedToRand(seed));
|
||||
seed = xorwise(seed);
|
||||
}
|
||||
@@ -159,6 +159,50 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
*/
|
||||
export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||
|
||||
export const randrun = (n) => {
|
||||
return signal((t) => {
|
||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||
const rands = timeToRands(t.floor().add(0.5), n);
|
||||
const nums = rands
|
||||
.map((n, i) => [n, i])
|
||||
.sort((a, b) => a[0] > b[0] - a[0] < b[0])
|
||||
.map((x) => x[1]);
|
||||
const i = t.cyclePos().mul(n).floor() % n;
|
||||
return nums[i];
|
||||
})._segment(n);
|
||||
};
|
||||
|
||||
const _rearrangeWith = (ipat, n, pat) => {
|
||||
const pats = [...Array(n).keys()].map((i) => pat.zoom(Fraction(i).div(n), Fraction(i + 1).div(n)));
|
||||
return ipat.fmap((i) => pats[i].repeatCycles(n)._fast(n)).innerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
* @name shuffle
|
||||
* Slices a pattern into the given number of parts, then plays those parts in random order.
|
||||
* Each part will be played exactly once per cycle.
|
||||
* @example
|
||||
* note("c d e f").sound("piano").shuffle(4)
|
||||
* @example
|
||||
* note("c d e f".shuffle(4), "g").sound("piano")
|
||||
*/
|
||||
export const shuffle = register('shuffle', (n, pat) => {
|
||||
return _rearrangeWith(randrun(n), n, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* @name scramble
|
||||
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
|
||||
* but parts might be played more than once, or not at all, per cycle.
|
||||
* @example
|
||||
* note("c d e f").sound("piano").scramble(4)
|
||||
* @example
|
||||
* note("c d e f".scramble(4), "g").sound("piano")
|
||||
*/
|
||||
export const scramble = register('scramble', (n, pat) => {
|
||||
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* A continuous pattern of random numbers, between 0 and 1.
|
||||
*
|
||||
@@ -326,6 +370,13 @@ export const pickRestart = register('pickRestart', function (lookup, pat) {
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a@2 b@2 c@2 d@2>".pickRestart({
|
||||
a: n("0 1 2 0"),
|
||||
b: n("2 3 4 ~"),
|
||||
c: n("[4 5] [4 3] 2 0"),
|
||||
d: n("0 -3 0 ~")
|
||||
}).scale("C:major").s("piano")
|
||||
*/
|
||||
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).restartJoin();
|
||||
|
||||
@@ -1248,4 +1248,9 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('loopAt', () => {
|
||||
it('maintains tactus', () => {
|
||||
expect(s('bev').chop(8).loopAt(2).tactus).toStrictEqual(Fraction(4));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,14 +61,6 @@ export class TimeSpan {
|
||||
// Applies given function to the end time of the timespan"""
|
||||
return new TimeSpan(this.begin, func_time(this.end));
|
||||
}
|
||||
withBegin(func_time) {
|
||||
// Applies given function to the end time of the timespan"""
|
||||
return new TimeSpan(func_time(this.begin), this.end);
|
||||
}
|
||||
withDuration(func_time) {
|
||||
const duration = this.end.sub(this.begin)
|
||||
return new TimeSpan(this.begin, this.begin.add(func_time(duration)))
|
||||
}
|
||||
|
||||
withCycle(func_time) {
|
||||
// Like withTime, but time is relative to relative to the cycle (i.e. the
|
||||
|
||||
@@ -91,6 +91,9 @@ export const midi2note = (n) => {
|
||||
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
|
||||
export const _mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
// average numbers in an array
|
||||
export const averageArray = (arr) => arr.reduce((a, b) => a + b) / arr.length;
|
||||
|
||||
export function nanFallback(value, fallback = 0) {
|
||||
if (isNaN(Number(value))) {
|
||||
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
|
||||
@@ -360,6 +363,76 @@ export function objectMap(obj, fn) {
|
||||
}
|
||||
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
||||
}
|
||||
export function cycleToSeconds(cycle, cps) {
|
||||
return cycle / cps;
|
||||
}
|
||||
|
||||
// utility for averaging two clocks together to account for drift
|
||||
export class ClockCollator {
|
||||
constructor({
|
||||
getTargetClockTime = getUnixTimeSeconds,
|
||||
weight = 16,
|
||||
offsetDelta = 0.005,
|
||||
checkAfterTime = 2,
|
||||
resetAfterTime = 8,
|
||||
}) {
|
||||
this.offsetTime;
|
||||
this.timeAtPrevOffsetSample;
|
||||
this.prevOffsetTimes = [];
|
||||
this.getTargetClockTime = getTargetClockTime;
|
||||
this.weight = weight;
|
||||
this.offsetDelta = offsetDelta;
|
||||
this.checkAfterTime = checkAfterTime;
|
||||
this.resetAfterTime = resetAfterTime;
|
||||
this.reset = () => {
|
||||
this.prevOffsetTimes = [];
|
||||
this.offsetTime = null;
|
||||
this.timeAtPrevOffsetSample = null;
|
||||
};
|
||||
}
|
||||
calculateOffset(currentTime) {
|
||||
const targetClockTime = this.getTargetClockTime();
|
||||
const diffBetweenTimeSamples = targetClockTime - this.timeAtPrevOffsetSample;
|
||||
const newOffsetTime = targetClockTime - currentTime;
|
||||
// recalcuate the diff from scratch if the clock has been paused for some time.
|
||||
if (diffBetweenTimeSamples > this.resetAfterTime) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if (this.offsetTime == null) {
|
||||
this.offsetTime = newOffsetTime;
|
||||
}
|
||||
this.prevOffsetTimes.push(newOffsetTime);
|
||||
if (this.prevOffsetTimes.length > this.weight) {
|
||||
this.prevOffsetTimes.shift();
|
||||
}
|
||||
|
||||
// after X time has passed, the average of the previous weight offset times is calculated and used as a stable reference
|
||||
// for calculating the timestamp
|
||||
if (this.timeAtPrevOffsetSample == null || diffBetweenTimeSamples > this.checkAfterTime) {
|
||||
this.timeAtPrevOffsetSample = targetClockTime;
|
||||
const rollingOffsetTime = averageArray(this.prevOffsetTimes);
|
||||
//when the clock offsets surpass the delta, set the new reference time
|
||||
if (Math.abs(rollingOffsetTime - this.offsetTime) > this.offsetDelta) {
|
||||
this.offsetTime = rollingOffsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
return this.offsetTime;
|
||||
}
|
||||
|
||||
calculateTimestamp(currentTime, targetTime) {
|
||||
return this.calculateOffset(currentTime) + targetTime;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPerformanceTimeSeconds() {
|
||||
return performance.now() * 0.001;
|
||||
}
|
||||
|
||||
function getUnixTimeSeconds() {
|
||||
return Date.now() * 0.001;
|
||||
}
|
||||
|
||||
// Floating point versions, see Fraction for rational versions
|
||||
// // greatest common divisor
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
|
||||
import { Pattern, ClockCollator } from '@strudel/core';
|
||||
import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs';
|
||||
import { Invoke } from './utils.mjs';
|
||||
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
||||
hap.ensureObjectValue();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
controls.note && (controls.note = parseNumeral(controls.note));
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
const params = [];
|
||||
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const params = [];
|
||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||
|
||||
const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime));
|
||||
Object.keys(controls).forEach((key) => {
|
||||
const val = controls[key];
|
||||
const value = typeof val === 'number' ? val.toString() : val;
|
||||
|
||||
Object.keys(controls).forEach((key) => {
|
||||
const val = controls[key];
|
||||
const value = typeof val === 'number' ? val.toString() : val;
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
params.push({
|
||||
name: key,
|
||||
value,
|
||||
valueisnumber: typeof val === 'number',
|
||||
});
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
params.push({
|
||||
name: key,
|
||||
value,
|
||||
valueisnumber: typeof val === 'number',
|
||||
});
|
||||
|
||||
const messagesfromjs = [];
|
||||
if (params.length) {
|
||||
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
|
||||
}
|
||||
|
||||
if (messagesfromjs.length) {
|
||||
setTimeout(() => {
|
||||
Invoke('sendosc', { messagesfromjs });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (params.length === 0) {
|
||||
return;
|
||||
}
|
||||
const message = { target: '/dirt/play', timestamp, params };
|
||||
setTimeout(() => {
|
||||
Invoke('sendosc', { messagesfromjs: [message] });
|
||||
});
|
||||
}
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(oscTriggerTauri);
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } =
|
||||
return silence;
|
||||
};
|
||||
|
||||
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'smear');
|
||||
export const { x, y, w, h, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', 'r', 'fill', 'smear');
|
||||
|
||||
export const rescale = register('rescale', function (f, pat) {
|
||||
return pat.mul(x(f).w(f).y(f).h(f));
|
||||
|
||||
@@ -169,13 +169,18 @@ const refs = {};
|
||||
export async function midin(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
|
||||
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(
|
||||
|
||||
@@ -14,7 +14,7 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
const options = ast.options_;
|
||||
const ops = options?.ops;
|
||||
|
||||
const tactus_source = pat.__tactus_source;
|
||||
if (ops) {
|
||||
for (const op of ops) {
|
||||
switch (op.type_) {
|
||||
@@ -69,7 +69,7 @@ const applyOptions = (parent, enter) => (pat, i) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pat.__tactus_source = pat.__tactus_source || tactus_source;
|
||||
return pat;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
"devDependencies": {
|
||||
"peggy": "^3.0.2",
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,12 +210,14 @@ describe('mini', () => {
|
||||
});
|
||||
it('supports ^ tactus marking', () => {
|
||||
expect(mini('a [^b c]').tactus).toEqual(Fraction(4));
|
||||
expect(mini('[^b c]!3').tactus).toEqual(Fraction(6));
|
||||
expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
|
||||
expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
|
||||
expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8));
|
||||
expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4));
|
||||
expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12));
|
||||
expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24));
|
||||
expect(mini('[^a b c d e]').tactus).toEqual(Fraction(5));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+40
-30
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import OSC from 'osc-js';
|
||||
|
||||
import { logger, parseNumeral, Pattern, getEventOffsetMs, isNote, noteToMidi } from '@strudel/core';
|
||||
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
||||
|
||||
let connection; // Promise<OSC>
|
||||
function connect() {
|
||||
@@ -34,6 +34,44 @@ function connect() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
export function parseControlsFromHap(hap, cps) {
|
||||
hap.ensureObjectValue();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
if (typeof controls.note !== 'undefined') {
|
||||
if (isNote(controls.note)) {
|
||||
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
||||
} else {
|
||||
controls.note = parseNumeral(controls.note);
|
||||
}
|
||||
}
|
||||
controls.bank && (controls.s = controls.bank + controls.s);
|
||||
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
||||
// speed adjustment for CPS is handled on the DSP side in superdirt and pattern side in Strudel,
|
||||
// so we need to undo the adjustment before sending the message to superdirt.
|
||||
controls.unit === 'c' && controls.speed != null && (controls.speed = controls.speed / cps);
|
||||
const channels = controls.channels;
|
||||
channels != undefined && (controls.channels = JSON.stringify(channels));
|
||||
return controls;
|
||||
}
|
||||
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
const osc = await connect();
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
|
||||
const ts = Math.round(collator.calculateTimestamp(currentTime, targetTime) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
osc.send(bundle);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||
@@ -44,33 +82,5 @@ function connect() {
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.osc = function () {
|
||||
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
|
||||
hap.ensureObjectValue();
|
||||
const osc = await connect();
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
if (typeof controls.note !== 'undefined') {
|
||||
if (isNote(controls.note)) {
|
||||
controls.midinote = noteToMidi(controls.note, controls.octave || 3);
|
||||
} else {
|
||||
controls.note = parseNumeral(controls.note);
|
||||
}
|
||||
}
|
||||
controls.bank && (controls.s = controls.bank + controls.s);
|
||||
controls.roomsize && (controls.size = parseNumeral(controls.roomsize));
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
const offset = getEventOffsetMs(targetTime, currentTime);
|
||||
|
||||
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
|
||||
const ts = Math.floor(Date.now() + offset);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
osc.send(bundle);
|
||||
});
|
||||
return this.onTrigger(oscTrigger);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs';
|
||||
import { oscTrigger } from './osc.mjs';
|
||||
|
||||
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
|
||||
|
||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
||||
const currentTime = performance.now() / 1000;
|
||||
return trigger(null, hap, currentTime, cps, targetTime);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ 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'),
|
||||
|
||||
@@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
||||
if (!(name in writeMessagers)) {
|
||||
getWriter(name, br);
|
||||
}
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
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 = (time - currentTime + latency) * 1000;
|
||||
const offset = (targetTime - currentTime + latency) * 1000;
|
||||
|
||||
window.setTimeout(function () {
|
||||
writeMessagers[name](message, chk);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# @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
|
||||
@@ -0,0 +1 @@
|
||||
export * from './shader.mjs';
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
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];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,488 @@
|
||||
'use strict';
|
||||
// sourced from https://github.com/indutny/fft.js/
|
||||
// LICENSE
|
||||
// This software is licensed under the MIT License.
|
||||
// Copyright Fedor Indutny, 2017.
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
export default class FFT {
|
||||
constructor(size) {
|
||||
this.size = size | 0;
|
||||
if (this.size <= 1 || (this.size & (this.size - 1)) !== 0)
|
||||
throw new Error('FFT size must be a power of two and bigger than 1');
|
||||
|
||||
this._csize = size << 1;
|
||||
|
||||
// NOTE: Use of `var` is intentional for old V8 versions
|
||||
var table = new Array(this.size * 2);
|
||||
for (var i = 0; i < table.length; i += 2) {
|
||||
const angle = (Math.PI * i) / this.size;
|
||||
table[i] = Math.cos(angle);
|
||||
table[i + 1] = -Math.sin(angle);
|
||||
}
|
||||
this.table = table;
|
||||
|
||||
// Find size's power of two
|
||||
var power = 0;
|
||||
for (var t = 1; this.size > t; t <<= 1) power++;
|
||||
|
||||
// Calculate initial step's width:
|
||||
// * If we are full radix-4 - it is 2x smaller to give inital len=8
|
||||
// * Otherwise it is the same as `power` to give len=4
|
||||
this._width = power % 2 === 0 ? power - 1 : power;
|
||||
|
||||
// Pre-compute bit-reversal patterns
|
||||
this._bitrev = new Array(1 << this._width);
|
||||
for (var j = 0; j < this._bitrev.length; j++) {
|
||||
this._bitrev[j] = 0;
|
||||
for (var shift = 0; shift < this._width; shift += 2) {
|
||||
var revShift = this._width - shift - 2;
|
||||
this._bitrev[j] |= ((j >>> shift) & 3) << revShift;
|
||||
}
|
||||
}
|
||||
|
||||
this._out = null;
|
||||
this._data = null;
|
||||
this._inv = 0;
|
||||
}
|
||||
fromComplexArray(complex, storage) {
|
||||
var res = storage || new Array(complex.length >>> 1);
|
||||
for (var i = 0; i < complex.length; i += 2) res[i >>> 1] = complex[i];
|
||||
return res;
|
||||
}
|
||||
createComplexArray() {
|
||||
const res = new Array(this._csize);
|
||||
for (var i = 0; i < res.length; i++) res[i] = 0;
|
||||
return res;
|
||||
}
|
||||
toComplexArray(input, storage) {
|
||||
var res = storage || this.createComplexArray();
|
||||
for (var i = 0; i < res.length; i += 2) {
|
||||
res[i] = input[i >>> 1];
|
||||
res[i + 1] = 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
completeSpectrum(spectrum) {
|
||||
var size = this._csize;
|
||||
var half = size >>> 1;
|
||||
for (var i = 2; i < half; i += 2) {
|
||||
spectrum[size - i] = spectrum[i];
|
||||
spectrum[size - i + 1] = -spectrum[i + 1];
|
||||
}
|
||||
}
|
||||
transform(out, data) {
|
||||
if (out === data) throw new Error('Input and output buffers must be different');
|
||||
|
||||
this._out = out;
|
||||
this._data = data;
|
||||
this._inv = 0;
|
||||
this._transform4();
|
||||
this._out = null;
|
||||
this._data = null;
|
||||
}
|
||||
realTransform(out, data) {
|
||||
if (out === data) throw new Error('Input and output buffers must be different');
|
||||
|
||||
this._out = out;
|
||||
this._data = data;
|
||||
this._inv = 0;
|
||||
this._realTransform4();
|
||||
this._out = null;
|
||||
this._data = null;
|
||||
}
|
||||
inverseTransform(out, data) {
|
||||
if (out === data) throw new Error('Input and output buffers must be different');
|
||||
|
||||
this._out = out;
|
||||
this._data = data;
|
||||
this._inv = 1;
|
||||
this._transform4();
|
||||
for (var i = 0; i < out.length; i++) out[i] /= this.size;
|
||||
this._out = null;
|
||||
this._data = null;
|
||||
}
|
||||
// radix-4 implementation
|
||||
//
|
||||
// NOTE: Uses of `var` are intentional for older V8 version that do not
|
||||
// support both `let compound assignments` and `const phi`
|
||||
_transform4() {
|
||||
var out = this._out;
|
||||
var size = this._csize;
|
||||
|
||||
// Initial step (permute and transform)
|
||||
var width = this._width;
|
||||
var step = 1 << width;
|
||||
var len = (size / step) << 1;
|
||||
|
||||
var outOff;
|
||||
var t;
|
||||
var bitrev = this._bitrev;
|
||||
if (len === 4) {
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t];
|
||||
this._singleTransform2(outOff, off, step);
|
||||
}
|
||||
} else {
|
||||
// len === 8
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t];
|
||||
this._singleTransform4(outOff, off, step);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through steps in decreasing order
|
||||
var inv = this._inv ? -1 : 1;
|
||||
var table = this.table;
|
||||
for (step >>= 2; step >= 2; step >>= 2) {
|
||||
len = (size / step) << 1;
|
||||
var quarterLen = len >>> 2;
|
||||
|
||||
// Loop through offsets in the data
|
||||
for (outOff = 0; outOff < size; outOff += len) {
|
||||
// Full case
|
||||
var limit = outOff + quarterLen;
|
||||
for (var i = outOff, k = 0; i < limit; i += 2, k += step) {
|
||||
const A = i;
|
||||
const B = A + quarterLen;
|
||||
const C = B + quarterLen;
|
||||
const D = C + quarterLen;
|
||||
|
||||
// Original values
|
||||
const Ar = out[A];
|
||||
const Ai = out[A + 1];
|
||||
const Br = out[B];
|
||||
const Bi = out[B + 1];
|
||||
const Cr = out[C];
|
||||
const Ci = out[C + 1];
|
||||
const Dr = out[D];
|
||||
const Di = out[D + 1];
|
||||
|
||||
// Middle values
|
||||
const MAr = Ar;
|
||||
const MAi = Ai;
|
||||
|
||||
const tableBr = table[k];
|
||||
const tableBi = inv * table[k + 1];
|
||||
const MBr = Br * tableBr - Bi * tableBi;
|
||||
const MBi = Br * tableBi + Bi * tableBr;
|
||||
|
||||
const tableCr = table[2 * k];
|
||||
const tableCi = inv * table[2 * k + 1];
|
||||
const MCr = Cr * tableCr - Ci * tableCi;
|
||||
const MCi = Cr * tableCi + Ci * tableCr;
|
||||
|
||||
const tableDr = table[3 * k];
|
||||
const tableDi = inv * table[3 * k + 1];
|
||||
const MDr = Dr * tableDr - Di * tableDi;
|
||||
const MDi = Dr * tableDi + Di * tableDr;
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = MAr + MCr;
|
||||
const T0i = MAi + MCi;
|
||||
const T1r = MAr - MCr;
|
||||
const T1i = MAi - MCi;
|
||||
const T2r = MBr + MDr;
|
||||
const T2i = MBi + MDi;
|
||||
const T3r = inv * (MBr - MDr);
|
||||
const T3i = inv * (MBi - MDi);
|
||||
|
||||
// Final values
|
||||
const FAr = T0r + T2r;
|
||||
const FAi = T0i + T2i;
|
||||
|
||||
const FCr = T0r - T2r;
|
||||
const FCi = T0i - T2i;
|
||||
|
||||
const FBr = T1r + T3i;
|
||||
const FBi = T1i - T3r;
|
||||
|
||||
const FDr = T1r - T3i;
|
||||
const FDi = T1i + T3r;
|
||||
|
||||
out[A] = FAr;
|
||||
out[A + 1] = FAi;
|
||||
out[B] = FBr;
|
||||
out[B + 1] = FBi;
|
||||
out[C] = FCr;
|
||||
out[C + 1] = FCi;
|
||||
out[D] = FDr;
|
||||
out[D + 1] = FDi;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// radix-2 implementation
|
||||
//
|
||||
// NOTE: Only called for len=4
|
||||
_singleTransform2(outOff, off, step) {
|
||||
const out = this._out;
|
||||
const data = this._data;
|
||||
|
||||
const evenR = data[off];
|
||||
const evenI = data[off + 1];
|
||||
const oddR = data[off + step];
|
||||
const oddI = data[off + step + 1];
|
||||
|
||||
const leftR = evenR + oddR;
|
||||
const leftI = evenI + oddI;
|
||||
const rightR = evenR - oddR;
|
||||
const rightI = evenI - oddI;
|
||||
|
||||
out[outOff] = leftR;
|
||||
out[outOff + 1] = leftI;
|
||||
out[outOff + 2] = rightR;
|
||||
out[outOff + 3] = rightI;
|
||||
}
|
||||
// radix-4
|
||||
//
|
||||
// NOTE: Only called for len=8
|
||||
_singleTransform4(outOff, off, step) {
|
||||
const out = this._out;
|
||||
const data = this._data;
|
||||
const inv = this._inv ? -1 : 1;
|
||||
const step2 = step * 2;
|
||||
const step3 = step * 3;
|
||||
|
||||
// Original values
|
||||
const Ar = data[off];
|
||||
const Ai = data[off + 1];
|
||||
const Br = data[off + step];
|
||||
const Bi = data[off + step + 1];
|
||||
const Cr = data[off + step2];
|
||||
const Ci = data[off + step2 + 1];
|
||||
const Dr = data[off + step3];
|
||||
const Di = data[off + step3 + 1];
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = Ar + Cr;
|
||||
const T0i = Ai + Ci;
|
||||
const T1r = Ar - Cr;
|
||||
const T1i = Ai - Ci;
|
||||
const T2r = Br + Dr;
|
||||
const T2i = Bi + Di;
|
||||
const T3r = inv * (Br - Dr);
|
||||
const T3i = inv * (Bi - Di);
|
||||
|
||||
// Final values
|
||||
const FAr = T0r + T2r;
|
||||
const FAi = T0i + T2i;
|
||||
|
||||
const FBr = T1r + T3i;
|
||||
const FBi = T1i - T3r;
|
||||
|
||||
const FCr = T0r - T2r;
|
||||
const FCi = T0i - T2i;
|
||||
|
||||
const FDr = T1r - T3i;
|
||||
const FDi = T1i + T3r;
|
||||
|
||||
out[outOff] = FAr;
|
||||
out[outOff + 1] = FAi;
|
||||
out[outOff + 2] = FBr;
|
||||
out[outOff + 3] = FBi;
|
||||
out[outOff + 4] = FCr;
|
||||
out[outOff + 5] = FCi;
|
||||
out[outOff + 6] = FDr;
|
||||
out[outOff + 7] = FDi;
|
||||
}
|
||||
// Real input radix-4 implementation
|
||||
_realTransform4() {
|
||||
var out = this._out;
|
||||
var size = this._csize;
|
||||
|
||||
// Initial step (permute and transform)
|
||||
var width = this._width;
|
||||
var step = 1 << width;
|
||||
var len = (size / step) << 1;
|
||||
|
||||
var outOff;
|
||||
var t;
|
||||
var bitrev = this._bitrev;
|
||||
if (len === 4) {
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t];
|
||||
this._singleRealTransform2(outOff, off >>> 1, step >>> 1);
|
||||
}
|
||||
} else {
|
||||
// len === 8
|
||||
for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {
|
||||
const off = bitrev[t];
|
||||
this._singleRealTransform4(outOff, off >>> 1, step >>> 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through steps in decreasing order
|
||||
var inv = this._inv ? -1 : 1;
|
||||
var table = this.table;
|
||||
for (step >>= 2; step >= 2; step >>= 2) {
|
||||
len = (size / step) << 1;
|
||||
var halfLen = len >>> 1;
|
||||
var quarterLen = halfLen >>> 1;
|
||||
var hquarterLen = quarterLen >>> 1;
|
||||
|
||||
// Loop through offsets in the data
|
||||
for (outOff = 0; outOff < size; outOff += len) {
|
||||
for (var i = 0, k = 0; i <= hquarterLen; i += 2, k += step) {
|
||||
var A = outOff + i;
|
||||
var B = A + quarterLen;
|
||||
var C = B + quarterLen;
|
||||
var D = C + quarterLen;
|
||||
|
||||
// Original values
|
||||
var Ar = out[A];
|
||||
var Ai = out[A + 1];
|
||||
var Br = out[B];
|
||||
var Bi = out[B + 1];
|
||||
var Cr = out[C];
|
||||
var Ci = out[C + 1];
|
||||
var Dr = out[D];
|
||||
var Di = out[D + 1];
|
||||
|
||||
// Middle values
|
||||
var MAr = Ar;
|
||||
var MAi = Ai;
|
||||
|
||||
var tableBr = table[k];
|
||||
var tableBi = inv * table[k + 1];
|
||||
var MBr = Br * tableBr - Bi * tableBi;
|
||||
var MBi = Br * tableBi + Bi * tableBr;
|
||||
|
||||
var tableCr = table[2 * k];
|
||||
var tableCi = inv * table[2 * k + 1];
|
||||
var MCr = Cr * tableCr - Ci * tableCi;
|
||||
var MCi = Cr * tableCi + Ci * tableCr;
|
||||
|
||||
var tableDr = table[3 * k];
|
||||
var tableDi = inv * table[3 * k + 1];
|
||||
var MDr = Dr * tableDr - Di * tableDi;
|
||||
var MDi = Dr * tableDi + Di * tableDr;
|
||||
|
||||
// Pre-Final values
|
||||
var T0r = MAr + MCr;
|
||||
var T0i = MAi + MCi;
|
||||
var T1r = MAr - MCr;
|
||||
var T1i = MAi - MCi;
|
||||
var T2r = MBr + MDr;
|
||||
var T2i = MBi + MDi;
|
||||
var T3r = inv * (MBr - MDr);
|
||||
var T3i = inv * (MBi - MDi);
|
||||
|
||||
// Final values
|
||||
var FAr = T0r + T2r;
|
||||
var FAi = T0i + T2i;
|
||||
|
||||
var FBr = T1r + T3i;
|
||||
var FBi = T1i - T3r;
|
||||
|
||||
out[A] = FAr;
|
||||
out[A + 1] = FAi;
|
||||
out[B] = FBr;
|
||||
out[B + 1] = FBi;
|
||||
|
||||
// Output final middle point
|
||||
if (i === 0) {
|
||||
var FCr = T0r - T2r;
|
||||
var FCi = T0i - T2i;
|
||||
out[C] = FCr;
|
||||
out[C + 1] = FCi;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not overwrite ourselves
|
||||
if (i === hquarterLen) continue;
|
||||
|
||||
// In the flipped case:
|
||||
// MAi = -MAi
|
||||
// MBr=-MBi, MBi=-MBr
|
||||
// MCr=-MCr
|
||||
// MDr=MDi, MDi=MDr
|
||||
var ST0r = T1r;
|
||||
var ST0i = -T1i;
|
||||
var ST1r = T0r;
|
||||
var ST1i = -T0i;
|
||||
var ST2r = -inv * T3i;
|
||||
var ST2i = -inv * T3r;
|
||||
var ST3r = -inv * T2i;
|
||||
var ST3i = -inv * T2r;
|
||||
|
||||
var SFAr = ST0r + ST2r;
|
||||
var SFAi = ST0i + ST2i;
|
||||
|
||||
var SFBr = ST1r + ST3i;
|
||||
var SFBi = ST1i - ST3r;
|
||||
|
||||
var SA = outOff + quarterLen - i;
|
||||
var SB = outOff + halfLen - i;
|
||||
|
||||
out[SA] = SFAr;
|
||||
out[SA + 1] = SFAi;
|
||||
out[SB] = SFBr;
|
||||
out[SB + 1] = SFBi;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// radix-2 implementation
|
||||
//
|
||||
// NOTE: Only called for len=4
|
||||
_singleRealTransform2(outOff, off, step) {
|
||||
const out = this._out;
|
||||
const data = this._data;
|
||||
|
||||
const evenR = data[off];
|
||||
const oddR = data[off + step];
|
||||
|
||||
const leftR = evenR + oddR;
|
||||
const rightR = evenR - oddR;
|
||||
|
||||
out[outOff] = leftR;
|
||||
out[outOff + 1] = 0;
|
||||
out[outOff + 2] = rightR;
|
||||
out[outOff + 3] = 0;
|
||||
}
|
||||
// radix-4
|
||||
//
|
||||
// NOTE: Only called for len=8
|
||||
_singleRealTransform4(outOff, off, step) {
|
||||
const out = this._out;
|
||||
const data = this._data;
|
||||
const inv = this._inv ? -1 : 1;
|
||||
const step2 = step * 2;
|
||||
const step3 = step * 3;
|
||||
|
||||
// Original values
|
||||
const Ar = data[off];
|
||||
const Br = data[off + step];
|
||||
const Cr = data[off + step2];
|
||||
const Dr = data[off + step3];
|
||||
|
||||
// Pre-Final values
|
||||
const T0r = Ar + Cr;
|
||||
const T1r = Ar - Cr;
|
||||
const T2r = Br + Dr;
|
||||
const T3r = inv * (Br - Dr);
|
||||
|
||||
// Final values
|
||||
const FAr = T0r + T2r;
|
||||
|
||||
const FBr = T1r;
|
||||
const FBi = -T3r;
|
||||
|
||||
const FCr = T0r - T2r;
|
||||
|
||||
const FDr = T1r;
|
||||
const FDi = T3r;
|
||||
|
||||
out[outOff] = FAr;
|
||||
out[outOff + 1] = 0;
|
||||
out[outOff + 2] = FBr;
|
||||
out[outOff + 3] = FBi;
|
||||
out[outOff + 4] = FCr;
|
||||
out[outOff + 5] = 0;
|
||||
out[outOff + 6] = FDr;
|
||||
out[outOff + 7] = FDi;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
'use strict';
|
||||
|
||||
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||
const WEBAUDIO_BLOCK_SIZE = 128;
|
||||
|
||||
/** Overlap-Add Node */
|
||||
class OLAProcessor extends AudioWorkletProcessor {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.started = false;
|
||||
this.nbInputs = options.numberOfInputs;
|
||||
this.nbOutputs = options.numberOfOutputs;
|
||||
|
||||
this.blockSize = options.processorOptions.blockSize;
|
||||
// TODO for now, the only support hop size is the size of a web audio block
|
||||
this.hopSize = WEBAUDIO_BLOCK_SIZE;
|
||||
|
||||
this.nbOverlaps = this.blockSize / this.hopSize;
|
||||
|
||||
// pre-allocate input buffers (will be reallocated if needed)
|
||||
this.inputBuffers = new Array(this.nbInputs);
|
||||
this.inputBuffersHead = new Array(this.nbInputs);
|
||||
this.inputBuffersToSend = new Array(this.nbInputs);
|
||||
// default to 1 channel per input until we know more
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
this.allocateInputChannels(i, 1);
|
||||
}
|
||||
// pre-allocate input buffers (will be reallocated if needed)
|
||||
this.outputBuffers = new Array(this.nbOutputs);
|
||||
this.outputBuffersToRetrieve = new Array(this.nbOutputs);
|
||||
// default to 1 channel per output until we know more
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
this.allocateOutputChannels(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles dynamic reallocation of input/output channels buffer
|
||||
(channel numbers may lety during lifecycle) **/
|
||||
reallocateChannelsIfNeeded(inputs, outputs) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
let nbChannels = inputs[i].length;
|
||||
if (nbChannels != this.inputBuffers[i].length) {
|
||||
this.allocateInputChannels(i, nbChannels);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
let nbChannels = outputs[i].length;
|
||||
if (nbChannels != this.outputBuffers[i].length) {
|
||||
this.allocateOutputChannels(i, nbChannels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allocateInputChannels(inputIndex, nbChannels) {
|
||||
// allocate input buffers
|
||||
|
||||
this.inputBuffers[inputIndex] = new Array(nbChannels);
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
|
||||
this.inputBuffers[inputIndex][i].fill(0);
|
||||
}
|
||||
|
||||
// allocate input buffers to send and head pointers to copy from
|
||||
// (cannot directly send a pointer/subarray because input may be modified)
|
||||
this.inputBuffersHead[inputIndex] = new Array(nbChannels);
|
||||
this.inputBuffersToSend[inputIndex] = new Array(nbChannels);
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize);
|
||||
this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize);
|
||||
}
|
||||
}
|
||||
|
||||
allocateOutputChannels(outputIndex, nbChannels) {
|
||||
// allocate output buffers
|
||||
this.outputBuffers[outputIndex] = new Array(nbChannels);
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize);
|
||||
this.outputBuffers[outputIndex][i].fill(0);
|
||||
}
|
||||
|
||||
// allocate output buffers to retrieve
|
||||
// (cannot send a pointer/subarray because new output has to be add to exising output)
|
||||
this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels);
|
||||
for (let i = 0; i < nbChannels; i++) {
|
||||
this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize);
|
||||
this.outputBuffersToRetrieve[outputIndex][i].fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Read next web audio block to input buffers **/
|
||||
readInputs(inputs) {
|
||||
// when playback is paused, we may stop receiving new samples
|
||||
if (inputs[0].length && inputs[0][0].length == 0) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffers[i][j].fill(0, this.blockSize);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
let webAudioBlock = inputs[i][j];
|
||||
this.inputBuffers[i][j].set(webAudioBlock, this.blockSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Write next web audio block from output buffers **/
|
||||
writeOutputs(outputs) {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE);
|
||||
outputs[i][j].set(webAudioBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of input buffers to receive new web audio block **/
|
||||
shiftInputBuffers() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift left content of output buffers to receive new web audio block **/
|
||||
shiftOutputBuffers() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE);
|
||||
this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy contents of input buffers to buffer actually sent to process **/
|
||||
prepareInputBuffersToSend() {
|
||||
for (let i = 0; i < this.nbInputs; i++) {
|
||||
for (let j = 0; j < this.inputBuffers[i].length; j++) {
|
||||
this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Add contents of output buffers just processed to output buffers **/
|
||||
handleOutputBuffersToRetrieve() {
|
||||
for (let i = 0; i < this.nbOutputs; i++) {
|
||||
for (let j = 0; j < this.outputBuffers[i].length; j++) {
|
||||
for (let k = 0; k < this.blockSize; k++) {
|
||||
this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process(inputs, outputs, params) {
|
||||
const input = inputs[0];
|
||||
const hasInput = !(input[0] === undefined);
|
||||
if (this.started && !hasInput) {
|
||||
return false;
|
||||
}
|
||||
this.started = hasInput;
|
||||
this.reallocateChannelsIfNeeded(inputs, outputs);
|
||||
|
||||
this.readInputs(inputs);
|
||||
this.shiftInputBuffers();
|
||||
this.prepareInputBuffersToSend();
|
||||
this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params);
|
||||
this.handleOutputBuffersToRetrieve();
|
||||
this.writeOutputs(outputs);
|
||||
this.shiftOutputBuffers();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
processOLA(inputs, outputs, params) {
|
||||
console.assert(false, 'Not overriden');
|
||||
}
|
||||
}
|
||||
|
||||
export default OLAProcessor;
|
||||
@@ -22,16 +22,13 @@ function humanFileSize(bytes, si) {
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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;
|
||||
let sampleUrl;
|
||||
let index = 0;
|
||||
if (Array.isArray(bank)) {
|
||||
@@ -50,19 +47,54 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
|
||||
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);
|
||||
}
|
||||
let buffer = await loadBuffer(sampleUrl, ac, s, index);
|
||||
if (speed < 0) {
|
||||
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) {
|
||||
// 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;
|
||||
return bufferSource;
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||
@@ -232,10 +264,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
||||
const { prebake, tag } = options;
|
||||
processSampleMap(
|
||||
sampleMap,
|
||||
(key, value) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
(key, bank) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
samples: bank,
|
||||
baseUrl,
|
||||
prebake,
|
||||
tag,
|
||||
@@ -249,38 +281,26 @@ 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 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);
|
||||
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
||||
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
@@ -292,26 +312,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
|
||||
const time = t + nudge;
|
||||
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) {
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
duration = (end - begin) * bufferDuration;
|
||||
duration = sliceDuration;
|
||||
}
|
||||
let holdEnd = t + duration;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { clamp, nanFallback, _mod } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
import workletsUrl from './worklets.mjs?worker&url';
|
||||
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
@@ -87,6 +87,10 @@ export const getAudioContext = () => {
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (!workletsLoading) {
|
||||
@@ -309,6 +313,13 @@ export function resetGlobalEffects() {
|
||||
}
|
||||
|
||||
export const superdough = async (value, t, hapDuration) => {
|
||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||
let { stretch } = value;
|
||||
if (stretch != null) {
|
||||
//account for phase vocoder latency
|
||||
const latency = 0.04;
|
||||
t = t - latency;
|
||||
}
|
||||
const ac = getAudioContext();
|
||||
if (typeof value !== 'object') {
|
||||
throw new Error(
|
||||
@@ -320,7 +331,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
// duration is passed as value too..
|
||||
value.duration = hapDuration;
|
||||
// calculate absolute time
|
||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||
|
||||
if (t < ac.currentTime) {
|
||||
console.warn(
|
||||
`[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`,
|
||||
@@ -364,7 +375,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
bandq = getDefaultValue('bandq'),
|
||||
channels = getDefaultValue('channels'),
|
||||
//phaser
|
||||
phaser,
|
||||
phaserrate: phaser,
|
||||
phaserdepth = getDefaultValue('phaserdepth'),
|
||||
phasersweep,
|
||||
phasercenter,
|
||||
@@ -410,6 +421,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
};
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
value.s = s;
|
||||
}
|
||||
|
||||
// get source AudioNode
|
||||
@@ -438,6 +450,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
}
|
||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
||||
chain.push(sourceNode);
|
||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
||||
|
||||
// gain stage
|
||||
chain.push(gainNode(gain));
|
||||
@@ -584,4 +597,6 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
|
||||
};
|
||||
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||
superdough(hap, t - ct, hap.duration / cps, cps);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
|
||||
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
|
||||
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
|
||||
|
||||
import OLAProcessor from './ola-processor';
|
||||
import FFT from './fft.js';
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
const _mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
@@ -464,3 +468,183 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
||||
|
||||
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||
const BUFFERED_BLOCK_SIZE = 2048;
|
||||
|
||||
function genHannWindow(length) {
|
||||
let win = new Float32Array(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
win[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / length));
|
||||
}
|
||||
return win;
|
||||
}
|
||||
|
||||
class PhaseVocoderProcessor extends OLAProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{
|
||||
name: 'pitchFactor',
|
||||
defaultValue: 1.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
options.processorOptions = {
|
||||
blockSize: BUFFERED_BLOCK_SIZE,
|
||||
};
|
||||
super(options);
|
||||
|
||||
this.fftSize = this.blockSize;
|
||||
this.timeCursor = 0;
|
||||
|
||||
this.hannWindow = genHannWindow(this.blockSize);
|
||||
// prepare FFT and pre-allocate buffers
|
||||
this.fft = new FFT(this.fftSize);
|
||||
this.freqComplexBuffer = this.fft.createComplexArray();
|
||||
this.freqComplexBufferShifted = this.fft.createComplexArray();
|
||||
this.timeComplexBuffer = this.fft.createComplexArray();
|
||||
this.magnitudes = new Float32Array(this.fftSize / 2 + 1);
|
||||
this.peakIndexes = new Int32Array(this.magnitudes.length);
|
||||
this.nbPeaks = 0;
|
||||
}
|
||||
|
||||
processOLA(inputs, outputs, parameters) {
|
||||
// no automation, take last value
|
||||
|
||||
let pitchFactor = parameters.pitchFactor[parameters.pitchFactor.length - 1];
|
||||
|
||||
if (pitchFactor < 0) {
|
||||
pitchFactor = pitchFactor * 0.25;
|
||||
}
|
||||
pitchFactor = Math.max(0, pitchFactor + 1);
|
||||
|
||||
for (var i = 0; i < this.nbInputs; i++) {
|
||||
for (var j = 0; j < inputs[i].length; j++) {
|
||||
// big assumption here: output is symetric to input
|
||||
var input = inputs[i][j];
|
||||
var output = outputs[i][j];
|
||||
|
||||
this.applyHannWindow(input);
|
||||
|
||||
this.fft.realTransform(this.freqComplexBuffer, input);
|
||||
|
||||
this.computeMagnitudes();
|
||||
this.findPeaks();
|
||||
this.shiftPeaks(pitchFactor);
|
||||
|
||||
this.fft.completeSpectrum(this.freqComplexBufferShifted);
|
||||
this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted);
|
||||
this.fft.fromComplexArray(this.timeComplexBuffer, output);
|
||||
this.applyHannWindow(output);
|
||||
}
|
||||
}
|
||||
|
||||
this.timeCursor += this.hopSize;
|
||||
}
|
||||
|
||||
/** Apply Hann window in-place */
|
||||
applyHannWindow(input) {
|
||||
for (var i = 0; i < this.blockSize; i++) {
|
||||
input[i] = input[i] * this.hannWindow[i] * 1.62;
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute squared magnitudes for peak finding **/
|
||||
computeMagnitudes() {
|
||||
var i = 0,
|
||||
j = 0;
|
||||
while (i < this.magnitudes.length) {
|
||||
let real = this.freqComplexBuffer[j];
|
||||
let imag = this.freqComplexBuffer[j + 1];
|
||||
// no need to sqrt for peak finding
|
||||
this.magnitudes[i] = real ** 2 + imag ** 2;
|
||||
i += 1;
|
||||
j += 2;
|
||||
}
|
||||
}
|
||||
|
||||
/** Find peaks in spectrum magnitudes **/
|
||||
findPeaks() {
|
||||
this.nbPeaks = 0;
|
||||
var i = 2;
|
||||
let end = this.magnitudes.length - 2;
|
||||
|
||||
while (i < end) {
|
||||
let mag = this.magnitudes[i];
|
||||
|
||||
if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
this.peakIndexes[this.nbPeaks] = i;
|
||||
this.nbPeaks++;
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift peaks and regions of influence by pitchFactor into new specturm */
|
||||
shiftPeaks(pitchFactor) {
|
||||
// zero-fill new spectrum
|
||||
this.freqComplexBufferShifted.fill(0);
|
||||
|
||||
for (var i = 0; i < this.nbPeaks; i++) {
|
||||
let peakIndex = this.peakIndexes[i];
|
||||
let peakIndexShifted = Math.round(peakIndex * pitchFactor);
|
||||
|
||||
if (peakIndexShifted > this.magnitudes.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// find region of influence
|
||||
var startIndex = 0;
|
||||
var endIndex = this.fftSize;
|
||||
if (i > 0) {
|
||||
let peakIndexBefore = this.peakIndexes[i - 1];
|
||||
startIndex = peakIndex - Math.floor((peakIndex - peakIndexBefore) / 2);
|
||||
}
|
||||
if (i < this.nbPeaks - 1) {
|
||||
let peakIndexAfter = this.peakIndexes[i + 1];
|
||||
endIndex = peakIndex + Math.ceil((peakIndexAfter - peakIndex) / 2);
|
||||
}
|
||||
|
||||
// shift whole region of influence around peak to shifted peak
|
||||
let startOffset = startIndex - peakIndex;
|
||||
let endOffset = endIndex - peakIndex;
|
||||
for (var j = startOffset; j < endOffset; j++) {
|
||||
let binIndex = peakIndex + j;
|
||||
let binIndexShifted = peakIndexShifted + j;
|
||||
|
||||
if (binIndexShifted >= this.magnitudes.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// apply phase correction
|
||||
let omegaDelta = (2 * Math.PI * (binIndexShifted - binIndex)) / this.fftSize;
|
||||
let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor);
|
||||
let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor);
|
||||
|
||||
let indexReal = binIndex * 2;
|
||||
let indexImag = indexReal + 1;
|
||||
let valueReal = this.freqComplexBuffer[indexReal];
|
||||
let valueImag = this.freqComplexBuffer[indexImag];
|
||||
|
||||
let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag;
|
||||
let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal;
|
||||
|
||||
let indexShiftedReal = binIndexShifted * 2;
|
||||
let indexShiftedImag = indexShiftedReal + 1;
|
||||
this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal;
|
||||
this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
|
||||
|
||||
@@ -37,6 +37,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+11469
-9230
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,13 @@ use std::net::UdpSocket;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use crate::loggerbridge::Logger;
|
||||
pub struct OscMsg {
|
||||
pub msg_buf: Vec<u8>,
|
||||
pub timestamp: u64,
|
||||
pub timestamp: f64,
|
||||
}
|
||||
|
||||
pub struct AsyncInputTransmit {
|
||||
@@ -106,7 +106,7 @@ pub struct Param {
|
||||
#[derive(Deserialize)]
|
||||
pub struct MessageFromJS {
|
||||
params: Vec<Param>,
|
||||
timestamp: u64,
|
||||
timestamp: f64,
|
||||
target: String,
|
||||
}
|
||||
// Called from JS
|
||||
@@ -127,9 +127,11 @@ pub async fn sendosc(
|
||||
args.push(OscType::String(p.value));
|
||||
}
|
||||
}
|
||||
// let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
|
||||
|
||||
let time_delay = Duration::from_secs_f64(m.timestamp);
|
||||
let duration_since_epoch =
|
||||
Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
|
||||
time_delay + Duration::new(UNIX_OFFSET, 0);
|
||||
|
||||
let seconds = u32::try_from(duration_since_epoch.as_secs())
|
||||
.map_err(|_| "bit conversion failed for osc message timetag")?;
|
||||
|
||||
@@ -967,6 +967,156 @@ 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 ]",
|
||||
@@ -1229,6 +1379,16 @@ 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 ]",
|
||||
@@ -1251,16 +1411,6 @@ exports[`runs examples > example "cat" example index 0 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 ]",
|
||||
@@ -2666,6 +2816,43 @@ 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 ]",
|
||||
@@ -3953,88 +4140,6 @@ exports[`runs examples > example "layer" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "legato" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | s:sawtooth ]",
|
||||
"[ 1/8 → 3/16 | s:sawtooth ]",
|
||||
"[ 3/16 → 5/16 | s:sawtooth ]",
|
||||
"[ 5/16 → 3/8 | s:sawtooth ]",
|
||||
"[ 3/8 → 1/2 | s:sawtooth ]",
|
||||
"[ 1/2 → 9/16 | s:sawtooth ]",
|
||||
"[ 9/16 → 11/16 | s:sawtooth ]",
|
||||
"[ 11/16 → 3/4 | s:sawtooth ]",
|
||||
"[ 3/4 → 7/8 | s:sawtooth ]",
|
||||
"[ 7/8 → 15/16 | s:sawtooth ]",
|
||||
"[ 15/16 → 1/1 | s:sawtooth ]",
|
||||
"[ 1/1 → 17/16 | s:sawtooth ]",
|
||||
"[ 9/8 → 37/32 | s:sawtooth ]",
|
||||
"[ 19/16 → 5/4 | s:sawtooth ]",
|
||||
"[ 21/16 → 43/32 | s:sawtooth ]",
|
||||
"[ 11/8 → 23/16 | s:sawtooth ]",
|
||||
"[ 3/2 → 49/32 | s:sawtooth ]",
|
||||
"[ 25/16 → 13/8 | s:sawtooth ]",
|
||||
"[ 27/16 → 55/32 | s:sawtooth ]",
|
||||
"[ 7/4 → 29/16 | s:sawtooth ]",
|
||||
"[ 15/8 → 61/32 | s:sawtooth ]",
|
||||
"[ 31/16 → 63/32 | s:sawtooth ]",
|
||||
"[ 2/1 → 17/8 | s:sawtooth ]",
|
||||
"[ 17/8 → 35/16 | s:sawtooth ]",
|
||||
"[ 35/16 → 37/16 | s:sawtooth ]",
|
||||
"[ 37/16 → 19/8 | s:sawtooth ]",
|
||||
"[ 19/8 → 5/2 | s:sawtooth ]",
|
||||
"[ 5/2 → 41/16 | s:sawtooth ]",
|
||||
"[ 41/16 → 43/16 | s:sawtooth ]",
|
||||
"[ 43/16 → 11/4 | s:sawtooth ]",
|
||||
"[ 11/4 → 23/8 | s:sawtooth ]",
|
||||
"[ 23/8 → 47/16 | s:sawtooth ]",
|
||||
"[ 47/16 → 3/1 | s:sawtooth ]",
|
||||
"[ 3/1 → 49/16 | s:sawtooth ]",
|
||||
"[ 25/8 → 101/32 | s:sawtooth ]",
|
||||
"[ 51/16 → 13/4 | s:sawtooth ]",
|
||||
"[ 53/16 → 107/32 | s:sawtooth ]",
|
||||
"[ 27/8 → 55/16 | s:sawtooth ]",
|
||||
"[ 7/2 → 113/32 | s:sawtooth ]",
|
||||
"[ 57/16 → 29/8 | s:sawtooth ]",
|
||||
"[ 59/16 → 119/32 | s:sawtooth ]",
|
||||
"[ 15/4 → 61/16 | s:sawtooth ]",
|
||||
"[ 31/8 → 125/32 | s:sawtooth ]",
|
||||
"[ 63/16 → 127/32 | s:sawtooth ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "legato" example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 3/16 | s:supersaw ]",
|
||||
"[ 3/16 → 5/16 | s:supersaw ]",
|
||||
"[ 5/16 → 7/16 | s:supersaw ]",
|
||||
"[ 7/16 → 5/8 | s:supersaw ]",
|
||||
"[ 5/8 → 3/4 | s:supersaw ]",
|
||||
"[ 3/4 → 7/8 | s:supersaw ]",
|
||||
"[ 7/8 → 1/1 | s:supersaw ]",
|
||||
"[ 1/1 → 19/16 | s:supersaw ]",
|
||||
"[ 19/16 → 21/16 | s:supersaw ]",
|
||||
"[ 21/16 → 23/16 | s:supersaw ]",
|
||||
"[ 23/16 → 13/8 | s:supersaw ]",
|
||||
"[ 13/8 → 7/4 | s:supersaw ]",
|
||||
"[ 7/4 → 15/8 | s:supersaw ]",
|
||||
"[ 15/8 → 2/1 | s:supersaw ]",
|
||||
"[ 2/1 → 35/16 | s:supersaw ]",
|
||||
"[ 35/16 → 37/16 | s:supersaw ]",
|
||||
"[ 37/16 → 39/16 | s:supersaw ]",
|
||||
"[ 39/16 → 21/8 | s:supersaw ]",
|
||||
"[ 21/8 → 11/4 | s:supersaw ]",
|
||||
"[ 11/4 → 23/8 | s:supersaw ]",
|
||||
"[ 23/8 → 3/1 | s:supersaw ]",
|
||||
"[ 3/1 → 51/16 | s:supersaw ]",
|
||||
"[ 51/16 → 53/16 | s:supersaw ]",
|
||||
"[ 53/16 → 55/16 | s:supersaw ]",
|
||||
"[ 55/16 → 29/8 | s:supersaw ]",
|
||||
"[ 29/8 → 15/4 | s:supersaw ]",
|
||||
"[ 15/4 → 31/8 | s:supersaw ]",
|
||||
"[ 31/8 → 4/1 | s:supersaw ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "leslie" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | n:0 s:supersquare leslie:0 ]",
|
||||
@@ -5019,149 +5124,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 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 ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "phasercenter" example index 0 1`] = `
|
||||
[
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "phaserdepth" example index 0 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 ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "phasersweep" example index 0 1`] = `
|
||||
[
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -5316,6 +5421,25 @@ exports[`runs examples > example "pickF" example index 1 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pickmodRestart" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:C3 s:piano ]",
|
||||
"[ 1/4 → 1/2 | note:D3 s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:E3 s:piano ]",
|
||||
"[ 3/4 → 1/1 | note:C3 s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:C3 s:piano ]",
|
||||
"[ 5/4 → 3/2 | note:D3 s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:E3 s:piano ]",
|
||||
"[ 7/4 → 2/1 | note:C3 s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:E3 s:piano ]",
|
||||
"[ 9/4 → 5/2 | note:F3 s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:G3 s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:E3 s:piano ]",
|
||||
"[ 13/4 → 7/2 | note:F3 s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:G3 s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pitchwheel" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/13 | note:C3 s:sawtooth cutoff:500 ]",
|
||||
@@ -6700,6 +6824,56 @@ exports[`runs examples > example "scope" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scramble
|
||||
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
|
||||
but parts might be played more than once, or not at all, per cycle." example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:e s:piano ]",
|
||||
"[ 1/4 → 1/2 | note:d s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:c s:piano ]",
|
||||
"[ 3/4 → 1/1 | note:d s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:e s:piano ]",
|
||||
"[ 5/4 → 3/2 | note:c s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:d s:piano ]",
|
||||
"[ 7/4 → 2/1 | note:e s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:f s:piano ]",
|
||||
"[ 9/4 → 5/2 | note:f s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:c s:piano ]",
|
||||
"[ 11/4 → 3/1 | note:c s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:d s:piano ]",
|
||||
"[ 13/4 → 7/2 | note:d s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:f s:piano ]",
|
||||
"[ 15/4 → 4/1 | note:e s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "scramble
|
||||
Slices a pattern into the given number of parts, then plays those parts at random. Similar to \`shuffle\`,
|
||||
but parts might be played more than once, or not at all, per cycle." example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:e s:piano ]",
|
||||
"[ 1/8 → 1/4 | note:d s:piano ]",
|
||||
"[ 1/4 → 3/8 | note:c s:piano ]",
|
||||
"[ 3/8 → 1/2 | note:d s:piano ]",
|
||||
"[ 1/2 → 1/1 | note:g s:piano ]",
|
||||
"[ 1/1 → 9/8 | note:e s:piano ]",
|
||||
"[ 9/8 → 5/4 | note:c s:piano ]",
|
||||
"[ 5/4 → 11/8 | note:d s:piano ]",
|
||||
"[ 11/8 → 3/2 | note:e s:piano ]",
|
||||
"[ 3/2 → 2/1 | note:g s:piano ]",
|
||||
"[ 2/1 → 17/8 | note:f s:piano ]",
|
||||
"[ 17/8 → 9/4 | note:f s:piano ]",
|
||||
"[ 9/4 → 19/8 | note:c s:piano ]",
|
||||
"[ 19/8 → 5/2 | note:c s:piano ]",
|
||||
"[ 5/2 → 3/1 | note:g s:piano ]",
|
||||
"[ 3/1 → 25/8 | note:d s:piano ]",
|
||||
"[ 25/8 → 13/4 | note:d s:piano ]",
|
||||
"[ 13/4 → 27/8 | note:f s:piano ]",
|
||||
"[ 27/8 → 7/2 | note:e s:piano ]",
|
||||
"[ 7/2 → 4/1 | note:g s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "segment" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/24 | note:40.25 ]",
|
||||
@@ -6802,6 +6976,27 @@ 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 ]",
|
||||
@@ -6842,24 +7037,23 @@ exports[`runs examples > example "seq" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "seq" example index 0 2`] = `
|
||||
exports[`runs examples > example "seqPLoop" 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 ]",
|
||||
"[ 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/4 → 23/8 | s:cp ]",
|
||||
"[ 3/1 → 25/8 | s:bd ]",
|
||||
"[ 27/8 → 7/2 | s:bd ]",
|
||||
"[ 15/4 → 31/8 | s:bd ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -6916,6 +7110,56 @@ exports[`runs examples > example "shape" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "shuffle
|
||||
Slices a pattern into the given number of parts, then plays those parts in random order.
|
||||
Each part will be played exactly once per cycle." example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c s:piano ]",
|
||||
"[ 1/4 → 1/2 | note:d s:piano ]",
|
||||
"[ 1/2 → 3/4 | note:e s:piano ]",
|
||||
"[ 3/4 → 1/1 | note:f s:piano ]",
|
||||
"[ 1/1 → 5/4 | note:c s:piano ]",
|
||||
"[ 5/4 → 3/2 | note:d s:piano ]",
|
||||
"[ 3/2 → 7/4 | note:e s:piano ]",
|
||||
"[ 7/4 → 2/1 | note:f s:piano ]",
|
||||
"[ 2/1 → 9/4 | note:c s:piano ]",
|
||||
"[ 9/4 → 5/2 | note:d s:piano ]",
|
||||
"[ 5/2 → 11/4 | note:e s:piano ]",
|
||||
"[ 11/4 → 3/1 | note:f s:piano ]",
|
||||
"[ 3/1 → 13/4 | note:c s:piano ]",
|
||||
"[ 13/4 → 7/2 | note:d s:piano ]",
|
||||
"[ 7/2 → 15/4 | note:e s:piano ]",
|
||||
"[ 15/4 → 4/1 | note:f s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "shuffle
|
||||
Slices a pattern into the given number of parts, then plays those parts in random order.
|
||||
Each part will be played exactly once per cycle." example index 1 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:c s:piano ]",
|
||||
"[ 1/8 → 1/4 | note:d s:piano ]",
|
||||
"[ 1/4 → 3/8 | note:e s:piano ]",
|
||||
"[ 3/8 → 1/2 | note:f s:piano ]",
|
||||
"[ 1/2 → 1/1 | note:g s:piano ]",
|
||||
"[ 1/1 → 9/8 | note:c s:piano ]",
|
||||
"[ 9/8 → 5/4 | note:d s:piano ]",
|
||||
"[ 5/4 → 11/8 | note:e s:piano ]",
|
||||
"[ 11/8 → 3/2 | note:f s:piano ]",
|
||||
"[ 3/2 → 2/1 | note:g s:piano ]",
|
||||
"[ 2/1 → 17/8 | note:c s:piano ]",
|
||||
"[ 17/8 → 9/4 | note:d s:piano ]",
|
||||
"[ 9/4 → 19/8 | note:e s:piano ]",
|
||||
"[ 19/8 → 5/2 | note:f s:piano ]",
|
||||
"[ 5/2 → 3/1 | note:g s:piano ]",
|
||||
"[ 3/1 → 25/8 | note:c s:piano ]",
|
||||
"[ 25/8 → 13/4 | note:d s:piano ]",
|
||||
"[ 13/4 → 27/8 | note:e s:piano ]",
|
||||
"[ 27/8 → 7/2 | note:f s:piano ]",
|
||||
"[ 7/2 → 4/1 | note:g s:piano ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "silence" example index 0 1`] = `[]`;
|
||||
|
||||
exports[`runs examples > example "sine" example index 0 1`] = `
|
||||
@@ -7486,6 +7730,27 @@ 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 ]",
|
||||
@@ -7526,27 +7791,6 @@ exports[`runs examples > example "stack" example index 0 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 ]",
|
||||
@@ -7568,6 +7812,23 @@ exports[`runs examples > example "steps" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "stretch" example index 0 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/3) ⇝ 1/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 0/1 ⇜ (1/3 → 2/3) ⇝ 1/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 0/1 ⇜ (2/3 → 1/1) | s:gm_flute stretch:0.5 ]",
|
||||
"[ (1/1 → 4/3) ⇝ 2/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 1/1 ⇜ (4/3 → 5/3) ⇝ 2/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 1/1 ⇜ (5/3 → 2/1) | s:gm_flute stretch:0.5 ]",
|
||||
"[ (2/1 → 7/3) ⇝ 3/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 2/1 ⇜ (7/3 → 8/3) ⇝ 3/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 2/1 ⇜ (8/3 → 3/1) | s:gm_flute stretch:0.5 ]",
|
||||
"[ (3/1 → 10/3) ⇝ 4/1 | s:gm_flute stretch:1 ]",
|
||||
"[ 3/1 ⇜ (10/3 → 11/3) ⇝ 4/1 | s:gm_flute stretch:2 ]",
|
||||
"[ 3/1 ⇜ (11/3 → 4/1) | s:gm_flute stretch:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "striate" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | s:numbers n:0 begin:0 end:0.16666666666666666 ]",
|
||||
|
||||
@@ -3,11 +3,6 @@ import { configDefaults, defineConfig } from 'vitest/config';
|
||||
/// <reference types="vitest" />
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// ...
|
||||
deps: {
|
||||
registerNodeLoader: true,
|
||||
},
|
||||
threads: false,
|
||||
reporters: 'verbose',
|
||||
isolate: false,
|
||||
silent: true,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
Vendored
-290
@@ -1,290 +0,0 @@
|
||||
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,3 +1,4 @@
|
||||
.astro
|
||||
# build output
|
||||
dist/
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/osc": "workspace:*",
|
||||
"@strudel/serial": "workspace:*",
|
||||
"@strudel/shader": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { loadFeaturedPatterns, loadPublicPatterns } from '@src/user_pattern_utils.mjs';
|
||||
import { MiniRepl } from '@src/docs/MiniRepl';
|
||||
import { PatternLabel } from '@src/repl/panel/PatternsTab';
|
||||
import { PatternLabel } from '@src/repl/components/panel/PatternsTab';
|
||||
|
||||
function PatternList({ patterns }) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function UdelFrame({ onEvaluate, hash, instance }) {
|
||||
const ref = useRef();
|
||||
window.addEventListener('message', (message) => {
|
||||
const childWindow = ref?.current?.contentWindow;
|
||||
if (message == null || message.source !== childWindow) {
|
||||
return; // Skip message in this event listener
|
||||
}
|
||||
onEvaluate(message.data);
|
||||
});
|
||||
|
||||
const url = new URL(window.location.origin);
|
||||
url.hash = hash;
|
||||
url.searchParams.append('instance', instance);
|
||||
const source = url.toString();
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={ref}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
minWidth: '50%',
|
||||
boxSizing: 'border-box',
|
||||
border: '0px',
|
||||
}}
|
||||
title="strudel embed"
|
||||
src={source}
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { code2hash } from '@strudel/core';
|
||||
|
||||
import { UdelFrame } from './UdelFrame';
|
||||
import { useState } from 'react';
|
||||
import UdelsHeader from './UdelsHeader';
|
||||
|
||||
const defaultHash = 'c3RhY2soCiAgCik%3D';
|
||||
|
||||
const getHashesFromUrl = () => {
|
||||
return window.location.hash?.slice(1).split(',');
|
||||
};
|
||||
const updateURLHashes = (hashes) => {
|
||||
const newHash = '#' + hashes.join(',');
|
||||
window.location.hash = newHash;
|
||||
};
|
||||
export function Udels() {
|
||||
const hashes = getHashesFromUrl();
|
||||
|
||||
const [numWindows, setNumWindows] = useState(hashes?.length ?? 1);
|
||||
const numWindowsOnChange = (num) => {
|
||||
setNumWindows(num);
|
||||
const hashes = getHashesFromUrl();
|
||||
const newHashes = [];
|
||||
for (let i = 0; i < num; i++) {
|
||||
newHashes[i] = hashes[i] ?? defaultHash;
|
||||
}
|
||||
updateURLHashes(newHashes);
|
||||
};
|
||||
|
||||
const onEvaluate = (key, code) => {
|
||||
const hashes = getHashesFromUrl();
|
||||
hashes[key] = code2hash(code);
|
||||
|
||||
updateURLHashes(hashes);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
height: '100vh',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<UdelsHeader numWindows={numWindows} setNumWindows={numWindowsOnChange} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{hashes.map((hash, key) => {
|
||||
return (
|
||||
<UdelFrame
|
||||
instance={key}
|
||||
onEvaluate={(code) => {
|
||||
onEvaluate(key, code);
|
||||
}}
|
||||
hash={hash}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
|
||||
// type Props = {
|
||||
// context: replcontext,
|
||||
// }
|
||||
|
||||
export default function UdelsEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
|
||||
|
||||
return (
|
||||
<div className={'h-full flex w-full flex-col relative'}>
|
||||
<Loader active={pending} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
<HorizontalPanel context={context} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import NumberInput from '@src/repl/components/NumberInput';
|
||||
|
||||
export default function UdelsHeader(Props) {
|
||||
const { numWindows, setNumWindows } = Props;
|
||||
|
||||
return (
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-900">
|
||||
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
|
||||
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
|
||||
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
|
||||
|
||||
<div className={'animate-pulse'}>
|
||||
<span className="">strudel</span> <span className="text-sm">-UDELS</span>
|
||||
</div>
|
||||
</h1>
|
||||
<NumberInput value={numWindows} setValue={setNumWindows} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -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 + interprete numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21
|
||||
- added \_asNumber + interpret 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 browers by @felixroos in https://github.com/tidalcycles/strudel/pull/311
|
||||
- fix: copy share link to clipboard was broken for some browsers 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 exisiting audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882
|
||||
- bugfix: suspend and close existing 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 trainling slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753
|
||||
- hopefully fix trailing 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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -37,7 +36,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];
|
||||
}
|
||||
@@ -120,6 +119,7 @@ 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,6 +129,7 @@ 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" />
|
||||
@@ -140,6 +141,7 @@ 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">
|
||||
@@ -150,6 +152,7 @@ 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" />
|
||||
|
||||
@@ -30,7 +30,7 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class="h-app-height text-gray-50 bg-background">
|
||||
<body class="h-app-height m-0 text-gray-50 bg-background">
|
||||
<div class="w-full h-full space-y-4 flex flex-col">
|
||||
<header class="max-w-full fixed top-0 w-full z-[100]">
|
||||
<Header currentPage={currentPage} />
|
||||
|
||||
@@ -3,12 +3,12 @@ import HeadCommon from '../components/HeadCommon.astro';
|
||||
import { Repl } from '../repl/Repl';
|
||||
---
|
||||
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en" class="m-0 dark">
|
||||
<head>
|
||||
<HeadCommon />
|
||||
<title>Strudel REPL</title>
|
||||
</head>
|
||||
<body class="h-app-height bg-background">
|
||||
<body class="h-app-height bg-background m-0">
|
||||
<Repl client:only="react" />
|
||||
<a rel="me" href="https://social.toplap.org/@strudel" target="_blank" class="hidden">mastodon</a>
|
||||
</body>
|
||||
|
||||
@@ -25,26 +25,14 @@ 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} />
|
||||
|
||||
@@ -45,7 +45,7 @@ But you can also control cc messages separately like this:
|
||||
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
|
||||
/>
|
||||
|
||||
# SuperDirt API
|
||||
# OSC/SuperDirt API
|
||||
|
||||
In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider.
|
||||
Strudel also supports using [SuperDirt](https://github.com/musikinformatik/SuperDirt/) as a backend, although it requires some developer tooling to run.
|
||||
@@ -73,16 +73,14 @@ Now you're all set!
|
||||
|
||||
If you now hear sound, congratulations! If not, you can get help on the [#strudel channel in the TidalCycles discord](https://discord.com/invite/HGEdXmRkzT).
|
||||
|
||||
Note: if you have the 'Audio Engine Target' in settings set to 'OSC', you do not need to add .osc() to the end of your pattern.
|
||||
|
||||
### Pattern.osc
|
||||
|
||||
<JsDoc client:idle name="Pattern.osc" h={0} />
|
||||
|
||||
## SuperDirt Params
|
||||
|
||||
The following functions can be used with [SuperDirt](https://github.com/musikinformatik/SuperDirt/):
|
||||
|
||||
`s n note freq channel orbit cutoff resonance hcutoff hresonance bandf bandq djf vowel cut begin end loop fadeTime speed unitA gain amp accelerate crush coarse delay lock leslie lrate lsize pan panspan pansplay room size dry shape squiz waveloss attack decay octave detune tremolodepth`
|
||||
|
||||
Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
|
||||
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import { Udels } from '../../components/Udels/Udels.jsx';
|
||||
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
---
|
||||
|
||||
|
||||
<body class="m-0">
|
||||
<Udels client:only="react" />
|
||||
</body>
|
||||
+9
-262
@@ -4,268 +4,15 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { code2hash, logger, silence } from '@strudel/core';
|
||||
import { getDrawContext } from '@strudel/draw';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import {
|
||||
getAudioContext,
|
||||
webaudioOutput,
|
||||
resetGlobalEffects,
|
||||
resetLoadedSounds,
|
||||
initAudioOnFirstClick,
|
||||
} from '@strudel/webaudio';
|
||||
import { defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
|
||||
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||
import { clearHydra } from '@strudel/hydra';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../settings.mjs';
|
||||
import {
|
||||
setActivePattern,
|
||||
setLatestCode,
|
||||
createPatternID,
|
||||
userPattern,
|
||||
getViewingPatternData,
|
||||
setViewingPatternData,
|
||||
} from '../user_pattern_utils.mjs';
|
||||
import { Header } from './Header';
|
||||
import Loader from './Loader';
|
||||
import { Panel } from './panel/Panel';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { prebake } from './prebake.mjs';
|
||||
import { getRandomTune, initCode, loadModules, shareCode, ReplContext } from './util.mjs';
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import './Repl.css';
|
||||
import { setInterval, clearInterval } from 'worker-timers';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
|
||||
let modulesLoading, presets, drawContext, clearCanvas, isIframe, audioReady;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
audioReady = initAudioOnFirstClick();
|
||||
modulesLoading = loadModules();
|
||||
presets = prebake();
|
||||
drawContext = getDrawContext();
|
||||
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
|
||||
isIframe = window.location !== window.parent.location;
|
||||
}
|
||||
|
||||
async function getModule(name) {
|
||||
if (!modulesLoading) {
|
||||
return;
|
||||
}
|
||||
const modules = await modulesLoading;
|
||||
return modules.find((m) => m.packageName === name);
|
||||
}
|
||||
import { isIframe, isUdels } from './util.mjs';
|
||||
import UdelsEditor from '@components/Udels/UdelsEditor';
|
||||
import ReplEditor from './components/ReplEditor';
|
||||
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
|
||||
import { useReplContext } from './useReplContext';
|
||||
|
||||
export function Repl({ embedded = false }) {
|
||||
const isEmbedded = embedded || isIframe;
|
||||
const { panelPosition, isZen, isSyncEnabled } = useSettings();
|
||||
const init = useCallback(() => {
|
||||
const drawTime = [-2, 2];
|
||||
const drawContext = getDrawContext();
|
||||
const editor = new StrudelMirror({
|
||||
sync: isSyncEnabled,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
transpiler,
|
||||
autodraw: false,
|
||||
root: containerRef.current,
|
||||
initialCode: '// LOADING',
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
drawContext,
|
||||
prebake: async () => Promise.all([modulesLoading, presets]),
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
onToggle: (playing) => {
|
||||
if (!playing) {
|
||||
clearHydra();
|
||||
}
|
||||
},
|
||||
beforeEval: () => audioReady,
|
||||
afterEval: (all) => {
|
||||
const { code } = all;
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
setDocumentTitle(code);
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setVersionDefaultsFrom(code);
|
||||
const data = { ...viewingPatternData, code };
|
||||
let id = data.id;
|
||||
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
||||
|
||||
if (isExamplePattern) {
|
||||
const codeHasChanged = code !== viewingPatternData.code;
|
||||
if (codeHasChanged) {
|
||||
// fork example
|
||||
const newPattern = userPattern.duplicate(data);
|
||||
id = newPattern.id;
|
||||
setViewingPatternData(newPattern.data);
|
||||
}
|
||||
} else {
|
||||
id = userPattern.isValidID(id) ? id : createPatternID();
|
||||
setViewingPatternData(userPattern.update(id, data).data);
|
||||
}
|
||||
setActivePattern(id);
|
||||
},
|
||||
bgFill: false,
|
||||
});
|
||||
window.strudelMirror = editor;
|
||||
|
||||
// init settings
|
||||
|
||||
initCode().then(async (decoded) => {
|
||||
let code, msg;
|
||||
if (decoded) {
|
||||
code = decoded;
|
||||
msg = `I have loaded the code from the URL.`;
|
||||
} else if (latestCode) {
|
||||
code = latestCode;
|
||||
msg = `Your last session has been loaded!`;
|
||||
} else {
|
||||
const { code: randomTune, name } = await getRandomTune();
|
||||
code = randomTune;
|
||||
msg = `A random code snippet named "${name}" has been loaded!`;
|
||||
}
|
||||
editor.setCode(code);
|
||||
setDocumentTitle(code);
|
||||
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
|
||||
});
|
||||
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const [replState, setReplState] = useState({});
|
||||
const { started, isDirty, error, activeCode, pending } = replState;
|
||||
const editorRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
// this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly!
|
||||
// this will be the case when the main repl is being replaced
|
||||
const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) });
|
||||
useEffect(() => {
|
||||
let editorSettings = {};
|
||||
Object.keys(defaultSettings).forEach((key) => {
|
||||
if (_settings.hasOwnProperty(key)) {
|
||||
editorSettings[key] = _settings[key];
|
||||
}
|
||||
});
|
||||
editorRef.current?.updateSettings(editorSettings);
|
||||
}, [_settings]);
|
||||
|
||||
// on first load, set stored audio device if possible
|
||||
useEffect(() => {
|
||||
const { audioDeviceName } = _settings;
|
||||
if (audioDeviceName !== defaultAudioDeviceName) {
|
||||
getAudioDevices().then((devices) => {
|
||||
const deviceID = devices.get(audioDeviceName);
|
||||
if (deviceID == null) {
|
||||
return;
|
||||
}
|
||||
setAudioDevice(deviceID);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
const setDocumentTitle = (code) => {
|
||||
const meta = getMetadata(code);
|
||||
document.title = (meta.title ? `${meta.title} - ` : '') + 'Strudel REPL';
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
editorRef.current?.toggle();
|
||||
};
|
||||
|
||||
const resetEditor = async () => {
|
||||
(await getModule('@strudel/tonal'))?.resetVoicings();
|
||||
resetGlobalEffects();
|
||||
clearCanvas();
|
||||
clearHydra();
|
||||
resetLoadedSounds();
|
||||
editorRef.current.repl.setCps(0.5);
|
||||
await prebake(); // declare default samples
|
||||
};
|
||||
|
||||
const handleUpdate = async (patternData, reset = false) => {
|
||||
setViewingPatternData(patternData);
|
||||
editorRef.current.setCode(patternData.code);
|
||||
if (reset) {
|
||||
await resetEditor();
|
||||
handleEvaluate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvaluate = () => {
|
||||
editorRef.current.evaluate();
|
||||
};
|
||||
const handleShuffle = async () => {
|
||||
const patternData = await getRandomTune();
|
||||
const code = patternData.code;
|
||||
logger(`[repl] ✨ loading random tune "${patternData.id}"`);
|
||||
setActivePattern(patternData.id);
|
||||
setViewingPatternData(patternData);
|
||||
await resetEditor();
|
||||
editorRef.current.setCode(code);
|
||||
editorRef.current.repl.evaluate(code);
|
||||
};
|
||||
|
||||
const handleShare = async () => shareCode(replState.code);
|
||||
const context = {
|
||||
embedded,
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
activeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
handleEvaluate,
|
||||
};
|
||||
|
||||
return (
|
||||
<ReplContext.Provider value={context}>
|
||||
<div className={cx('h-full flex flex-col relative')}>
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
{isEmbedded && !started && (
|
||||
<button
|
||||
onClick={() => handleTogglePlay()}
|
||||
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
|
||||
>
|
||||
<PlayCircleIcon className="w-6 h-6" />
|
||||
<span>play</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<section
|
||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')}
|
||||
id="code"
|
||||
ref={(el) => {
|
||||
containerRef.current = el;
|
||||
if (!editorRef.current) {
|
||||
init();
|
||||
}
|
||||
}}
|
||||
></section>
|
||||
{panelPosition === 'right' && !isEmbedded && <Panel context={context} />}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>
|
||||
)}
|
||||
{panelPosition === 'bottom' && !isEmbedded && <Panel context={context} />}
|
||||
</div>
|
||||
</ReplContext.Provider>
|
||||
);
|
||||
const isEmbedded = embedded || isIframe();
|
||||
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
|
||||
const context = useReplContext();
|
||||
return <Editor context={context} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
|
||||
// type Props = {
|
||||
// started: boolean;
|
||||
// handleTogglePlay: () => void;
|
||||
// };
|
||||
export default function BigPlayButton(Props) {
|
||||
const { started, handleTogglePlay } = Props;
|
||||
if (started) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => handleTogglePlay()}
|
||||
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
|
||||
>
|
||||
<PlayCircleIcon className="w-6 h-6" />
|
||||
<span>play</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// type Props = {
|
||||
// containerRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// init: () => void
|
||||
// }
|
||||
export function Code(Props) {
|
||||
const { editorRef, containerRef, init } = Props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow'}
|
||||
id="code"
|
||||
ref={(el) => {
|
||||
containerRef.current = el;
|
||||
if (!editorRef.current) {
|
||||
init();
|
||||
}
|
||||
}}
|
||||
></section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import BigPlayButton from '@src/repl/components/BigPlayButton';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
import { Header } from './Header';
|
||||
|
||||
// type Props = {
|
||||
// context: replcontext,
|
||||
// }
|
||||
|
||||
export default function EmbeddedReplEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
|
||||
return (
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} embedded={true} />
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,24 +5,15 @@ import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
|
||||
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
||||
import cx from '@src/cx.mjs';
|
||||
import { useSettings, setIsZen } from '../settings.mjs';
|
||||
// import { ReplContext } from './Repl';
|
||||
import './Repl.css';
|
||||
import { useSettings, setIsZen } from '../../settings.mjs';
|
||||
import '../Repl.css';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||
|
||||
export function Header({ context }) {
|
||||
const {
|
||||
embedded,
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
activeCode,
|
||||
handleTogglePlay,
|
||||
handleEvaluate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
} = context;
|
||||
export function Header({ context, embedded = false }) {
|
||||
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
|
||||
context;
|
||||
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
|
||||
const { isZen } = useSettings();
|
||||
|
||||
@@ -52,7 +43,7 @@ export function Header({ context }) {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer')}
|
||||
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer', isZen && 'fixed top-2 right-4')}
|
||||
onClick={() => {
|
||||
if (!isEmbedded) {
|
||||
setIsZen(!isZen);
|
||||
@@ -0,0 +1,54 @@
|
||||
function Button(Props) {
|
||||
const { children, onClick } = Props;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
data-input-counter-increment="counter-input"
|
||||
className="flex-shrink-0 bg-gray-700 hover:bg-gray-600 border-gray-600 inline-flex items-center justify-center border rounded-md h-5 w-5 focus:ring-gray-700 focus:ring-2 focus:outline-none"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
export default function NumberInput(Props) {
|
||||
const { value = 0, setValue, max, min } = Props;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<Button onClick={() => setValue(value - 1)}>
|
||||
<svg
|
||||
className="w-2.5 h-2.5 text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 2"
|
||||
>
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 1h16" />
|
||||
</svg>
|
||||
</Button>
|
||||
<input
|
||||
min={min}
|
||||
max={max}
|
||||
type="text"
|
||||
data-input-counter
|
||||
className="flex-shrink-0 text-white border-0 bg-transparent text-sm font-normal focus:outline-none focus:ring-0 max-w-[2.5rem] text-center"
|
||||
placeholder=""
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => setValue(value + 1)}>
|
||||
<svg
|
||||
className="w-2.5 h-2.5 text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path strokeLinecap="round" stroke="currentColor" strokeLinejoin="round" strokeWidth="2" d="M9 1v16M1 9h16" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import Loader from '@src/repl/components/Loader';
|
||||
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
|
||||
import { Code } from '@src/repl/components/Code';
|
||||
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
|
||||
import { Header } from './Header';
|
||||
import { useSettings } from '@src/settings.mjs';
|
||||
|
||||
// type Props = {
|
||||
// context: replcontext,
|
||||
// }
|
||||
|
||||
export default function ReplEditor(Props) {
|
||||
const { context } = Props;
|
||||
const { containerRef, editorRef, error, init, pending } = context;
|
||||
const settings = useSettings();
|
||||
const { panelPosition } = settings;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
{panelPosition === 'right' && <VerticalPanel context={context} />}
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
{panelPosition === 'bottom' && <HorizontalPanel context={context} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// type Props = { error: Error | null };
|
||||
export default function UserFacingErrorMessage(Props) {
|
||||
const { error } = Props;
|
||||
if (error == null) {
|
||||
return;
|
||||
}
|
||||
return <div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>;
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getAudioDevices, setAudioDevice } from '../util.mjs';
|
||||
import { getAudioDevices, setAudioDevice } from '../../util.mjs';
|
||||
import { SelectInput } from './SelectInput';
|
||||
|
||||
const initdevices = new Map();
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { audioEngineTargets } from '../../../settings.mjs';
|
||||
import { SelectInput } from './SelectInput';
|
||||
|
||||
// Allows the user to select an audio interface for Strudel to play through
|
||||
export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
|
||||
const onTargetChange = (target) => {
|
||||
onChange(target);
|
||||
};
|
||||
const options = new Map([
|
||||
[audioEngineTargets.webaudio, audioEngineTargets.webaudio],
|
||||
[audioEngineTargets.osc, audioEngineTargets.osc],
|
||||
]);
|
||||
return (
|
||||
<div className=" flex flex-col gap-1">
|
||||
<SelectInput isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} />
|
||||
{target === audioEngineTargets.osc && (
|
||||
<div>
|
||||
<p className="text-sm italic">
|
||||
⚠ All events routed to OSC, audio is silenced! See{' '}
|
||||
<a className="text-blue-500" href="https://strudel.cc/learn/input-output/">
|
||||
Docs
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -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">
|
||||
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm py-2">
|
||||
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
|
||||
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
|
||||
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
|
||||
╚════██║ ██║ ██╔══██╗██║ ██║██║ ██║██╔══╝ ██║
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Fragment, useEffect } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { isAudioFile, readDir, dir, playFile } from '../files.mjs';
|
||||
import { isAudioFile, readDir, dir, playFile } from '../../files.mjs';
|
||||
|
||||
export function FilesTab() {
|
||||
const [path, setPath] = useState([]);
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../idbutils.mjs';
|
||||
import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../../idbutils.mjs';
|
||||
|
||||
//choose a directory to locally import samples
|
||||
export default function ImportSoundsButton({ onComplete }) {
|
||||
@@ -0,0 +1,187 @@
|
||||
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 { ConsoleTab } from './ConsoleTab';
|
||||
import { FilesTab } from './FilesTab';
|
||||
import { Reference } from './Reference';
|
||||
import { SettingsTab } from './SettingsTab';
|
||||
import { SoundsTab } from './SoundsTab';
|
||||
import { WelcomeTab } from './WelcomeTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
import { ChevronLeftIcon } from '@heroicons/react/16/solid';
|
||||
|
||||
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 }) {
|
||||
const [log, setLog] = useState([]);
|
||||
useLogger(
|
||||
useCallback((e) => {
|
||||
const { message, type, data } = e.detail;
|
||||
setLog((l) => {
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||
if (type === 'loaded-sample') {
|
||||
// const loadIndex = l.length - 1;
|
||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
l[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
onClick={onClick}
|
||||
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',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function PinButton({ pinned, setPinned }) {
|
||||
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',
|
||||
)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
+9
-9
@@ -5,13 +5,13 @@ import {
|
||||
useActivePattern,
|
||||
useViewingPatternData,
|
||||
userPattern,
|
||||
} from '../../user_pattern_utils.mjs';
|
||||
} from '../../../user_pattern_utils.mjs';
|
||||
import { useMemo } from 'react';
|
||||
import { getMetadata } from '../../metadata_parser';
|
||||
import { useExamplePatterns } from '../useExamplePatterns';
|
||||
import { parseJSON } from '../util.mjs';
|
||||
import { getMetadata } from '../../../metadata_parser.js';
|
||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||
import { parseJSON, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { settingsMap, useSettings } from '../../settings.mjs';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
@@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
|
||||
function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
return (
|
||||
<button className="hover:opacity-50" onClick={onClick} title={label}>
|
||||
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
@@ -99,10 +99,10 @@ export function PatternsTab({ context }) {
|
||||
};
|
||||
const viewingPatternID = viewingPatternData?.id;
|
||||
|
||||
const autoResetPatternOnChange = !window.parent?.location.pathname.includes('oodles');
|
||||
const autoResetPatternOnChange = !isUdels();
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
@@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="flex overflow-y-scroll max-h-full flex-col">
|
||||
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<PatternButtons
|
||||
onClick={(id) =>
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
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>
|
||||
<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 ">
|
||||
<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
|
||||
you can already make music with a small set of functions!
|
||||
</p>
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<section key={i}>
|
||||
<h3 id={`doc-${i}`}>{entry.name}</h3>
|
||||
{!!entry.synonyms_text && (
|
||||
<p>
|
||||
Synonyms: <code>{entry.synonyms_text}</code>
|
||||
</p>
|
||||
)}
|
||||
{/* <small>{entry.meta.filename}</small> */}
|
||||
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
|
||||
<ul>
|
||||
{entry.params?.map(({ name, type, description }, i) => (
|
||||
<li key={i}>
|
||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{entry.examples?.map((example, j) => (
|
||||
<pre key={j}>{example}</pre>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+47
-14
@@ -1,12 +1,15 @@
|
||||
import { defaultSettings, settingsMap, useSettings } from '../../settings.mjs';
|
||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { themes } from '@strudel/codemirror';
|
||||
import { isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||
import { confirmDialog } from '../../util.mjs';
|
||||
|
||||
function Checkbox({ label, value, onChange }) {
|
||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||
return (
|
||||
<label>
|
||||
<input type="checkbox" checked={value} onChange={onChange} />
|
||||
<input disabled={disabled} type="checkbox" checked={value} onChange={onChange} />
|
||||
{' ' + label}
|
||||
</label>
|
||||
);
|
||||
@@ -77,6 +80,8 @@ const fontFamilyOptions = {
|
||||
galactico: 'galactico',
|
||||
};
|
||||
|
||||
const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||
|
||||
export function SettingsTab({ started }) {
|
||||
const {
|
||||
theme,
|
||||
@@ -95,19 +100,41 @@ export function SettingsTab({ started }) {
|
||||
fontFamily,
|
||||
panelPosition,
|
||||
audioDeviceName,
|
||||
audioEngineTarget,
|
||||
} = useSettings();
|
||||
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4">
|
||||
{AudioContext.prototype.setSinkId != null && (
|
||||
<div className="text-foreground p-4 space-y-4 w-full">
|
||||
{canChangeAudioDevice && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
isDisabled={started}
|
||||
audioDeviceName={audioDeviceName}
|
||||
onChange={(audioDeviceName) => settingsMap.setKey('audioDeviceName', audioDeviceName)}
|
||||
onChange={(audioDeviceName) => {
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
settingsMap.setKey('audioDeviceName', audioDeviceName);
|
||||
return window.location.reload();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
<FormItem label="Audio Engine Target">
|
||||
<AudioEngineTargetSelector
|
||||
target={audioEngineTarget}
|
||||
onChange={(target) => {
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
settingsMap.setKey('audioEngineTarget', target);
|
||||
return window.location.reload();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Theme">
|
||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||
</FormItem>
|
||||
@@ -192,11 +219,15 @@ export function SettingsTab({ started }) {
|
||||
<Checkbox
|
||||
label="Sync across Browser Tabs / Windows"
|
||||
onChange={(cbEvent) => {
|
||||
if (confirm('Changing this setting requires the window to reload itself. OK?')) {
|
||||
settingsMap.setKey('isSyncEnabled', cbEvent.target.checked);
|
||||
window.location.reload();
|
||||
}
|
||||
const newVal = cbEvent.target.checked;
|
||||
confirmDialog(RELOAD_MSG).then((r) => {
|
||||
if (r) {
|
||||
settingsMap.setKey('isSyncEnabled', newVal);
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={shouldAlwaysSync}
|
||||
value={isSyncEnabled}
|
||||
/>
|
||||
</FormItem>
|
||||
@@ -205,9 +236,11 @@ export function SettingsTab({ started }) {
|
||||
<button
|
||||
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
|
||||
onClick={() => {
|
||||
if (confirm('Sure?')) {
|
||||
settingsMap.set(defaultSettings);
|
||||
}
|
||||
confirmDialog('Sure?').then((r) => {
|
||||
if (r) {
|
||||
settingsMap.set(defaultSettings);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
restore default settings
|
||||
+24
-8
@@ -1,8 +1,8 @@
|
||||
import useEvent from '@src/useEvent.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { settingsMap, useSettings } from '../../settings.mjs';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
|
||||
@@ -12,15 +12,20 @@ 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(([key, { data }]) => !data.prebake);
|
||||
return filtered.filter(([_, { data }]) => !data.prebake);
|
||||
}
|
||||
if (soundsFilter === 'drums') {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
||||
@@ -32,9 +37,11 @@ export function SoundsTab() {
|
||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||
}
|
||||
return filtered;
|
||||
}, [sounds, soundsFilter]);
|
||||
}, [sounds, soundsFilter, search]);
|
||||
|
||||
// holds mutable ref to current triggered sound
|
||||
const trigRef = useRef();
|
||||
|
||||
// stop current sound on mouseup
|
||||
useEvent('mouseup', () => {
|
||||
const t = trigRef.current;
|
||||
@@ -43,8 +50,16 @@ 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}
|
||||
@@ -58,7 +73,8 @@ 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">
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
+1
-1
@@ -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 max-w-[600px] pt-2 font-sans pb-8 px-4">
|
||||
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
|
||||
<h3>
|
||||
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||
</h3>
|
||||
@@ -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, value) => {
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value, fileResolver(subpath)), {
|
||||
processSampleMap(sampleMap, (key, bank) => {
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank, fileResolver(subpath)), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
samples: bank,
|
||||
fileSystem: true,
|
||||
tag: 'local',
|
||||
});
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
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, useLayoutEffect, useEffect, useRef, useState } from 'react';
|
||||
import { setActiveFooter, useSettings } from '../../settings.mjs';
|
||||
import { ConsoleTab } from './ConsoleTab';
|
||||
import { FilesTab } from './FilesTab';
|
||||
import { Reference } from './Reference';
|
||||
import { SettingsTab } from './SettingsTab';
|
||||
import { SoundsTab } from './SoundsTab';
|
||||
import { WelcomeTab } from './WelcomeTab';
|
||||
import { PatternsTab } from './PatternsTab';
|
||||
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 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;
|
||||
setLog((l) => {
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const id = nanoid(12);
|
||||
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
|
||||
if (type === 'loaded-sample') {
|
||||
// const loadIndex = l.length - 1;
|
||||
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
|
||||
l[loadIndex] = { message, type, id, data };
|
||||
} else if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, id, data }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
}, []),
|
||||
);
|
||||
|
||||
const PanelTab = ({ children, name, label }) => (
|
||||
<>
|
||||
<div
|
||||
onClick={() => setActiveFooter(name)}
|
||||
className={cx(
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
|
||||
activeFooter === name ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label || name}
|
||||
</div>
|
||||
{activeFooter === name && <>{children}</>}
|
||||
</>
|
||||
);
|
||||
const client = useClient();
|
||||
if (isZen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function useLogger(onTrigger) {
|
||||
useEvent(logger.key, onTrigger);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import jsdocJson from '../../../../doc.json';
|
||||
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));
|
||||
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Reference() {
|
||||
return (
|
||||
<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 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
|
||||
you can already make music with a small set of functions!
|
||||
</p>
|
||||
{visibleFunctions.map((entry, i) => (
|
||||
<section key={i}>
|
||||
<h3 id={`doc-${i}`}>{entry.name}</h3>
|
||||
{!!entry.synonyms_text && (
|
||||
<p>
|
||||
Synonyms: <code>{entry.synonyms_text}</code>
|
||||
</p>
|
||||
)}
|
||||
{/* <small>{entry.meta.filename}</small> */}
|
||||
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
|
||||
<ul>
|
||||
{entry.params?.map(({ name, type, description }, i) => (
|
||||
<li key={i}>
|
||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{entry.examples?.map((example, j) => (
|
||||
<pre key={j}>{example}</pre>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
Repl.jsx - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/App.js>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||
import { getDrawContext } from '@strudel/draw';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import {
|
||||
getAudioContextCurrentTime,
|
||||
webaudioOutput,
|
||||
resetGlobalEffects,
|
||||
resetLoadedSounds,
|
||||
initAudioOnFirstClick,
|
||||
} from '@strudel/webaudio';
|
||||
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
|
||||
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||
import { clearHydra } from '@strudel/hydra';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../settings.mjs';
|
||||
import {
|
||||
setActivePattern,
|
||||
setLatestCode,
|
||||
createPatternID,
|
||||
userPattern,
|
||||
getViewingPatternData,
|
||||
setViewingPatternData,
|
||||
} from '../user_pattern_utils.mjs';
|
||||
import { superdirtOutput } from '@strudel/osc/superdirtoutput';
|
||||
import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { prebake } from './prebake.mjs';
|
||||
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
|
||||
import './Repl.css';
|
||||
import { setInterval, clearInterval } from 'worker-timers';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
audioReady = initAudioOnFirstClick();
|
||||
modulesLoading = loadModules();
|
||||
presets = prebake();
|
||||
drawContext = getDrawContext();
|
||||
clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width);
|
||||
}
|
||||
|
||||
async function getModule(name) {
|
||||
if (!modulesLoading) {
|
||||
return;
|
||||
}
|
||||
const modules = await modulesLoading;
|
||||
return modules.find((m) => m.packageName === name);
|
||||
}
|
||||
|
||||
export function useReplContext() {
|
||||
const { isSyncEnabled, audioEngineTarget } = useSettings();
|
||||
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
|
||||
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
||||
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
||||
|
||||
const init = useCallback(() => {
|
||||
const drawTime = [-2, 2];
|
||||
const drawContext = getDrawContext();
|
||||
const editor = new StrudelMirror({
|
||||
sync: isSyncEnabled,
|
||||
defaultOutput,
|
||||
getTime,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
transpiler,
|
||||
autodraw: false,
|
||||
root: containerRef.current,
|
||||
initialCode: '// LOADING',
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
drawContext,
|
||||
prebake: async () => Promise.all([modulesLoading, presets]),
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
onToggle: (playing) => {
|
||||
if (!playing) {
|
||||
clearHydra();
|
||||
}
|
||||
},
|
||||
beforeEval: () => audioReady,
|
||||
afterEval: (all) => {
|
||||
const { code } = all;
|
||||
//post to iframe parent (like Udels) if it exists...
|
||||
window.parent?.postMessage(code);
|
||||
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
setDocumentTitle(code);
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setVersionDefaultsFrom(code);
|
||||
const data = { ...viewingPatternData, code };
|
||||
let id = data.id;
|
||||
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
||||
|
||||
if (isExamplePattern) {
|
||||
const codeHasChanged = code !== viewingPatternData.code;
|
||||
if (codeHasChanged) {
|
||||
// fork example
|
||||
const newPattern = userPattern.duplicate(data);
|
||||
id = newPattern.id;
|
||||
setViewingPatternData(newPattern.data);
|
||||
}
|
||||
} else {
|
||||
id = userPattern.isValidID(id) ? id : createPatternID();
|
||||
setViewingPatternData(userPattern.update(id, data).data);
|
||||
}
|
||||
setActivePattern(id);
|
||||
},
|
||||
bgFill: false,
|
||||
});
|
||||
window.strudelMirror = editor;
|
||||
|
||||
// init settings
|
||||
initCode().then(async (decoded) => {
|
||||
let code, msg;
|
||||
if (decoded) {
|
||||
code = decoded;
|
||||
msg = `I have loaded the code from the URL.`;
|
||||
} else if (latestCode) {
|
||||
code = latestCode;
|
||||
msg = `Your last session has been loaded!`;
|
||||
} else {
|
||||
const { code: randomTune, name } = await getRandomTune();
|
||||
code = randomTune;
|
||||
msg = `A random code snippet named "${name}" has been loaded!`;
|
||||
}
|
||||
editor.setCode(code);
|
||||
setDocumentTitle(code);
|
||||
logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight');
|
||||
});
|
||||
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const [replState, setReplState] = useState({});
|
||||
const { started, isDirty, error, activeCode, pending } = replState;
|
||||
const editorRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
// this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly!
|
||||
// this will be the case when the main repl is being replaced
|
||||
const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) });
|
||||
useEffect(() => {
|
||||
let editorSettings = {};
|
||||
Object.keys(defaultSettings).forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(_settings, key)) {
|
||||
editorSettings[key] = _settings[key];
|
||||
}
|
||||
});
|
||||
editorRef.current?.updateSettings(editorSettings);
|
||||
}, [_settings]);
|
||||
|
||||
// on first load, set stored audio device if possible
|
||||
useEffect(() => {
|
||||
const { audioDeviceName } = _settings;
|
||||
if (audioDeviceName !== defaultAudioDeviceName) {
|
||||
getAudioDevices().then((devices) => {
|
||||
const deviceID = devices.get(audioDeviceName);
|
||||
if (deviceID == null) {
|
||||
return;
|
||||
}
|
||||
setAudioDevice(deviceID);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
const setDocumentTitle = (code) => {
|
||||
const meta = getMetadata(code);
|
||||
document.title = (meta.title ? `${meta.title} - ` : '') + 'Strudel REPL';
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
editorRef.current?.toggle();
|
||||
};
|
||||
|
||||
const resetEditor = async () => {
|
||||
(await getModule('@strudel/tonal'))?.resetVoicings();
|
||||
resetGlobalEffects();
|
||||
clearCanvas();
|
||||
clearHydra();
|
||||
resetLoadedSounds();
|
||||
editorRef.current.repl.setCps(0.5);
|
||||
await prebake(); // declare default samples
|
||||
};
|
||||
|
||||
const handleUpdate = async (patternData, reset = false) => {
|
||||
setViewingPatternData(patternData);
|
||||
editorRef.current.setCode(patternData.code);
|
||||
if (reset) {
|
||||
await resetEditor();
|
||||
handleEvaluate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvaluate = () => {
|
||||
editorRef.current.evaluate();
|
||||
};
|
||||
const handleShuffle = async () => {
|
||||
const patternData = await getRandomTune();
|
||||
const code = patternData.code;
|
||||
logger(`[repl] ✨ loading random tune "${patternData.id}"`);
|
||||
setActivePattern(patternData.id);
|
||||
setViewingPatternData(patternData);
|
||||
await resetEditor();
|
||||
editorRef.current.setCode(code);
|
||||
editorRef.current.repl.evaluate(code);
|
||||
};
|
||||
|
||||
const handleShare = async () => shareCode(replState.code);
|
||||
const context = {
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
activeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
handleEvaluate,
|
||||
init,
|
||||
error,
|
||||
editorRef,
|
||||
containerRef,
|
||||
};
|
||||
return context;
|
||||
}
|
||||
+48
-25
@@ -2,14 +2,11 @@ import { evalScope, hash2code, logger } from '@strudel/core';
|
||||
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio';
|
||||
import { getMetadata } from '../metadata_parser';
|
||||
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import './Repl.css';
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { createContext } from 'react';
|
||||
import { $featuredPatterns, loadDBPatterns } from '@src/user_pattern_utils.mjs';
|
||||
|
||||
// Create a single supabase client for interacting with your database
|
||||
@@ -81,6 +78,7 @@ export function loadModules() {
|
||||
import('@strudel/codemirror'),
|
||||
import('@strudel/hydra'),
|
||||
import('@strudel/serial'),
|
||||
import('@strudel/shader'),
|
||||
import('@strudel/soundfonts'),
|
||||
import('@strudel/csound'),
|
||||
import('@strudel/tidal'),
|
||||
@@ -97,6 +95,16 @@ export function loadModules() {
|
||||
|
||||
return evalScope(settingPatterns, ...modules);
|
||||
}
|
||||
// confirm dialog is a promise in webkit and a boolean in other browsers... normalize it to be a promise everywhere
|
||||
export function confirmDialog(msg) {
|
||||
const confirmed = confirm(msg);
|
||||
if (confirmed instanceof Promise) {
|
||||
return confirmed;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
resolve(confirmed);
|
||||
});
|
||||
}
|
||||
|
||||
let lastShared;
|
||||
export async function shareCode(codeToShare) {
|
||||
@@ -105,34 +113,49 @@ export async function shareCode(codeToShare) {
|
||||
logger(`Link already generated!`, 'error');
|
||||
return;
|
||||
}
|
||||
const isPublic = confirm(
|
||||
|
||||
confirmDialog(
|
||||
'Do you want your pattern to be public? If no, press cancel and you will get just a private link.',
|
||||
);
|
||||
// generate uuid in the browser
|
||||
const hash = nanoid(12);
|
||||
const shareUrl = window.location.origin + window.location.pathname + '?' + hash;
|
||||
const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]);
|
||||
if (!error) {
|
||||
lastShared = codeToShare;
|
||||
// copy shareUrl to clipboard
|
||||
if (isTauri()) {
|
||||
await writeText(shareUrl);
|
||||
).then(async (isPublic) => {
|
||||
const hash = nanoid(12);
|
||||
const shareUrl = window.location.origin + window.location.pathname + '?' + hash;
|
||||
const { error } = await supabase.from('code_v1').insert([{ code: codeToShare, hash, ['public']: isPublic }]);
|
||||
if (!error) {
|
||||
lastShared = codeToShare;
|
||||
// copy shareUrl to clipboard
|
||||
if (isTauri()) {
|
||||
await writeText(shareUrl);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
}
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
alert(message);
|
||||
// alert(message);
|
||||
logger(message, 'highlight');
|
||||
} else {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
logger(message);
|
||||
}
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
alert(message);
|
||||
// alert(message);
|
||||
logger(message, 'highlight');
|
||||
} else {
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
logger(message);
|
||||
});
|
||||
}
|
||||
|
||||
export const isIframe = () => window.location !== window.parent.location;
|
||||
function isCrossOriginFrame() {
|
||||
try {
|
||||
return !window.top.location.hostname;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const ReplContext = createContext(null);
|
||||
export const isUdels = () => {
|
||||
if (isCrossOriginFrame()) {
|
||||
return false;
|
||||
}
|
||||
return window.top?.location?.pathname.includes('udels');
|
||||
};
|
||||
|
||||
export const getAudioDevices = async () => {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { persistentMap } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { register } from '@strudel/core';
|
||||
import { isUdels } from './repl/util.mjs';
|
||||
|
||||
export const defaultAudioDeviceName = 'System Standard';
|
||||
|
||||
export const audioEngineTargets = {
|
||||
webaudio: 'webaudio',
|
||||
osc: 'osc',
|
||||
};
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
@@ -24,12 +30,22 @@ export const defaultSettings = {
|
||||
isZen: false,
|
||||
soundsFilter: 'all',
|
||||
patternFilter: 'community',
|
||||
panelPosition: 'right',
|
||||
panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom',
|
||||
isPanelPinned: true,
|
||||
userPatterns: '{}',
|
||||
audioDeviceName: defaultAudioDeviceName,
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
};
|
||||
|
||||
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
|
||||
let search = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
search = new URLSearchParams(window.location.search);
|
||||
}
|
||||
// if running multiple instance in one window, it will use the settings for that instance. else default to normal
|
||||
const instance = parseInt(search?.get('instance') ?? '0');
|
||||
const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
|
||||
|
||||
export const settingsMap = persistentMap(settings_key, defaultSettings);
|
||||
|
||||
const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
|
||||
|
||||
@@ -45,6 +61,7 @@ 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),
|
||||
@@ -54,14 +71,15 @@ export function useSettings() {
|
||||
isTooltipEnabled: parseBoolean(state.isTooltipEnabled),
|
||||
isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled),
|
||||
isFlashEnabled: parseBoolean(state.isFlashEnabled),
|
||||
isSyncEnabled: parseBoolean(state.isSyncEnabled),
|
||||
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
|
||||
fontSize: Number(state.fontSize),
|
||||
panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
||||
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
||||
userPatterns: userPatterns,
|
||||
};
|
||||
}
|
||||
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
export const setPanelPinned = (isPinned) => settingsMap.setKey('isPanelPinned', isPinned);
|
||||
|
||||
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
}
|
||||
:root {
|
||||
--app-height: 100vh;
|
||||
--app-width: 100vw;
|
||||
}
|
||||
|
||||
#console-tab {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from '@strudel/core';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { settingsMap } from './settings.mjs';
|
||||
import { parseJSON, supabase } from './repl/util.mjs';
|
||||
import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
|
||||
|
||||
export let $publicPatterns = atom([]);
|
||||
export let $featuredPatterns = atom([]);
|
||||
@@ -131,17 +130,19 @@ export const userPattern = {
|
||||
return this.update(newPattern.id, { ...newPattern.data, code: data.code });
|
||||
},
|
||||
clearAll() {
|
||||
if (!confirm(`This will delete all your patterns. Are you really sure?`)) {
|
||||
return;
|
||||
}
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setUserPatterns({});
|
||||
confirmDialog(`This will delete all your patterns. Are you really sure?`).then((r) => {
|
||||
if (r == false) {
|
||||
return;
|
||||
}
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setUserPatterns({});
|
||||
|
||||
if (viewingPatternData.collection !== this.collection) {
|
||||
return { id: viewingPatternData.id, data: viewingPatternData };
|
||||
}
|
||||
setActivePattern(null);
|
||||
return this.create();
|
||||
if (viewingPatternData.collection !== this.collection) {
|
||||
return { id: viewingPatternData.id, data: viewingPatternData };
|
||||
}
|
||||
setActivePattern(null);
|
||||
return this.create();
|
||||
});
|
||||
},
|
||||
delete(id) {
|
||||
const userPatterns = this.getAll();
|
||||
|
||||
@@ -34,6 +34,7 @@ module.exports = {
|
||||
},
|
||||
spacing: {
|
||||
'app-height': 'var(--app-height)',
|
||||
'app-width': 'var(--app-width)',
|
||||
},
|
||||
typography(theme) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user