Compare commits

..

110 Commits

Author SHA1 Message Date
alex 85ee97c9ef minimini starting to work with some passing tests and highlights 2024-10-04 18:52:40 +01:00
alex 479041ebb6 paste in proto minimini 2024-10-04 16:00:36 +01:00
Felix Roos ad080d02b3 Merge pull request #1189 from kdiab/main
remove redundant example for cat, update snapshot
2024-09-27 00:17:00 +02:00
Khalid e9e7e02547 Fixed access to merged description in factories.mdx 2024-09-26 17:58:59 -04:00
Felix Roos 2ffc6c9601 Merge pull request #1185 from Bubobubobubobubo/search-bar
Adding search bar (soundtab.jsx)
2024-09-25 13:18:19 +02:00
Raphaël Forment 3a9349d9f5 format code for tests 2024-09-25 12:59:18 +02:00
Raphaël Forment bd3f2845f8 Reverting code 2024-09-25 12:57:20 +02:00
Alex McLean 2d320c459b Screenreader improvements (#1158)
* hide ascii art for screen readers
* aria-labels for minirepl buttons
2024-09-23 16:18:34 +01:00
Raphaël Forment ff9f4a8a0d Simplify search bar code 2024-09-22 23:40:27 +02:00
Raphaël Forment d7b94807cb Merge branch 'tidalcycles:main' into search-bar 2024-09-22 23:30:11 +02:00
kdiab 2c16a37a68 Combine examples for stack, match wording for cat, stack and seq 2024-09-22 10:42:46 -04:00
kdiab 717e4a2c1d Combine examples for seq and remove extra entry 2024-09-22 10:31:50 -04:00
kdiab c225b4719f Combine examples for cat 2024-09-21 11:01:00 -04:00
kdiab 25f762c393 remove redundant example for cat, update snapshot 2024-09-20 23:38:21 -04:00
Alex McLean b75561e3c7 Fix serial timing (#1188)
* fix serial timing
* format
2024-09-20 21:26:41 +01:00
Alex McLean abaab89061 Add bite function (#1187)
For patterning slices of patterns. ref #31.
2024-09-19 23:26:30 +01:00
Felix Roos e409086b34 Merge pull request #1147 from tidalcycles/bettern-zen-spacing
better spacing in zen mode
2024-09-14 13:30:53 +02:00
Felix Roos 532f1435d9 revert lockfile 2024-09-14 13:15:44 +02:00
Raphaël Forment 6cb61ade03 Adding search bar in soundtab.jsx 2024-09-14 11:39:59 +02:00
Felix Roos a40219ba06 Merge pull request #1039 from tidalcycles/filter
add filter + filterWhen + within
2024-09-14 10:50:23 +02:00
Felix Roos 9c99f5ada2 Merge pull request #1101 from tidalcycles/sampler-refactoring
refactor sampler
2024-09-14 10:49:21 +02:00
Felix Roos 2d72c8faa0 Merge pull request #1146 from tidalcycles/midi-error-msg
handle midin device not found error
2024-09-14 10:43:17 +02:00
Felix Roos b188fc6b6d Merge branch 'main' into sampler-refactoring 2024-09-14 10:38:27 +02:00
Martín Rodríguez bfb22228cc Add a search bar to the REPL Reference tab (#1165)
* Add a search bar to the REPL Reference tab
Simple text input that sticks to the top.
Filters by just checking if the search term is in the entry name or synonyms, if available.
2024-09-13 20:59:23 +01:00
Edward Betts 9714d88d61 Correct spelling mistakes (#1183) 2024-09-13 20:58:47 +01:00
Alex McLean bdddb91510 add seqPLoop, ref #31 (#1182) 2024-09-07 22:41:29 +01:00
Jade (Rose) Rowland 5bf1805534 Merge pull request #1180 from daslyfe/phaser_controls
make phaser control match superdirt
2024-09-04 23:52:50 -04:00
Jade (Rose) Rowland 202685e8c4 update test snapshot 2024-09-04 23:50:48 -04:00
Jade (Rose) Rowland aba633e16e fix 2024-09-04 23:42:38 -04:00
Jade (Rose) Rowland da45ce689c fixed formatting 2024-09-04 23:36:20 -04:00
Jade (Rose) Rowland acea7e435e Merge pull request #1179 from tidalcycles/revert-1178-consistent_phaser_controls
Revert "Make phaser control consistent with superdirt"
2024-09-04 23:33:41 -04:00
Jade (Rose) Rowland cb4810cbbc Revert "Make phaser control consistent with superdirt" 2024-09-04 23:32:35 -04:00
Jade (Rose) Rowland 5ec205b383 Merge pull request #1178 from daslyfe/consistent_phaser_controls
Make phaser control consistent with superdirt
2024-09-04 23:29:11 -04:00
Jade (Rose) Rowland ebc0b293a9 updated 2024-09-04 23:20:42 -04:00
Jade (Rose) Rowland 4608c321bd Merge pull request #1172 from daslyfe/speed-cps-fix
fix sample speed when using splice and fit with superdirt
2024-09-02 22:37:15 -04:00
Jade (Rose) Rowland c4243f41c6 spelling 2024-09-01 16:15:55 -04:00
Jade (Rose) Rowland d95b8bc667 Merge pull request #1160 from daslyfe/audio_target_selector
Create audio target selector for OSC/Superdirt
2024-09-01 10:02:24 -04:00
Alex McLean 263c13c36e Fixes fit so it works after a chop or slice (#1171)
* fix fit so it works after a chop or slice

* format
2024-09-01 14:03:47 +01:00
Jade (Rose) Rowland ddceb36613 prettier 2024-08-31 23:49:56 -04:00
Jade (Rose) Rowland ff8ec628be spelling 2024-08-31 23:43:07 -04:00
Jade (Rose) Rowland 11a07df120 add docs for OSC target 2024-08-31 23:38:23 -04:00
Jade (Rose) Rowland 8befa4382b Merge branch 'main' into audio_target_selector 2024-08-31 22:20:53 -04:00
Alex McLean 39006e4d71 polyJoin (#1168)
* create polymetric join - `polyJoin`
* add `poly` as an op alignment to support e.g. `"c e f [g a]".add.poly("0 12 7")`
2024-08-30 13:24:08 +01:00
Alex McLean 812ed6b60d snapshot 2024-08-23 16:12:28 +01:00
Alex McLean 8b6c8eb35d Add scramble and shuffle (#1167)
* add scramble and shuffle functions from Tidal
2024-08-23 16:05:10 +01:00
Jade (Rose) Rowland dcda9a5753 cleanup 2024-08-18 13:21:37 -04:00
Jade (Rose) Rowland d3733d4209 prettier 2024-08-18 13:07:41 -04:00
Jade (Rose) Rowland ef6af96947 Merge branch 'tidalcycles:main' into audio_target_selector 2024-08-18 12:39:28 -04:00
Jade (Rose) Rowland d86ff0a536 Merge pull request #1164 from daslyfe/improve_neocyclist_timing
Improve + simplify neocyclist timing
2024-08-18 12:22:20 -04:00
Jade (Rose) Rowland 2c9ea03fde prettier 2024-08-18 12:08:40 -04:00
Jade (Rose) Rowland 4eae366bee cleaning up 2024-08-18 12:05:03 -04:00
Jade (Rose) Rowland 117f7fa4b5 cleaning up 2024-08-17 17:26:46 -04:00
Jade (Rose) Rowland 73ee84a1fb working 2024-08-17 17:11:51 -04:00
Jade (Rose) Rowland 9c899d5308 testing 2024-08-17 15:31:21 -04:00
Jade (Rose) Rowland b60beaf508 prettier 2024-08-16 10:56:09 -04:00
Jade (Rose) Rowland 796f741a92 use performance clock for osc mode 2024-08-16 10:46:18 -04:00
Jade (Rose) Rowland 8da1d9fbf3 reset collator after n seconds 2024-08-16 00:44:18 -04:00
Jade (Rose) Rowland 5345986b5b Merge branch 'main' into audio_target_selector 2024-08-15 16:28:19 -04:00
Jade (Rose) Rowland 3945abd7e9 Merge pull request #1163 from daslyfe/cross_orgin_check
containerize/seperate out boolean checks for repl types/Repl logic into bespoke components.
2024-08-14 23:18:33 -04:00
Jade (Rose) Rowland 053e4ee54b Merge branch 'tidalcycles:main' into cross_orgin_check 2024-08-12 20:50:15 -04:00
Jade (Rose) Rowland 25d61a2e31 prettier 2024-08-12 20:38:29 -04:00
Jade (Rose) Rowland 5e6131af24 reorganized repl 2024-08-12 20:32:58 -04:00
Jade (Rose) Rowland 2899ee5aef testing: 2024-08-12 18:11:52 -04:00
Jade (Rose) Rowland 9d1674b13c Merge pull request #1162 from daslyfe/cors_pathname_issue
[CORS HOTFIX]
2024-08-12 12:57:21 -04:00
Jade (Rose) Rowland bfead58212 prettier 2024-08-12 12:56:04 -04:00
Jade (Rose) Rowland 926c893c4f check iframe 2024-08-12 12:54:05 -04:00
Jade (Rose) Rowland 3cd31e4841 fixed prettier 2024-08-11 12:50:14 -04:00
Jade (Rose) Rowland c2be2ed76a fixed osc clock 2024-08-11 12:40:38 -04:00
Jade (Rose) Rowland ac0dc7a8de remove desktop restriction 2024-08-11 11:04:20 -04:00
Jade (Rose) Rowland cba9169b2f update select dialog 2024-08-11 11:01:17 -04:00
Jade (Rose) Rowland e9a0a06b77 fixed dialog 2024-08-11 00:57:49 -04:00
Jade (Rose) Rowland d80c06cd55 fix confirm dialog 2024-08-11 00:35:20 -04:00
Jade (Rose) Rowland d850726910 Merge pull request #1157 from daslyfe/osc_clock_jitter
Fix OSC clock jitter
2024-08-08 23:11:28 -04:00
Jade (Rose) Rowland 1b01f41ffd fix test 2024-08-08 23:10:39 -04:00
Jade (Rose) Rowland ad00fa65f8 Merge branch 'tidalcycles:main' into osc_clock_jitter 2024-08-08 22:53:07 -04:00
Jade (Rose) Rowland 814babbf7a cleaning up 2024-08-08 22:52:06 -04:00
Jade (Rose) Rowland 28334ec94d rolling clock average 2024-08-08 20:55:49 -04:00
Jade (Rose) Rowland 7d2110d75f clock fail strategy 2024-08-08 20:35:12 -04:00
Jade (Rose) Rowland 94ccfa631e offset calc 2024-08-08 17:25:45 -04:00
Jack Armitage c7c56a1357 fix 2024-08-06 12:25:52 +00:00
Jack Armitage d40e4dac2f pickRestart example 2024-08-03 18:48:30 +00:00
Jade (Rose) Rowland 686247f0cb Merge branch 'main' of github.com:daslyfe/strudel 2024-07-28 13:33:57 -04:00
Felix Roos dffeac41af Merge remote-tracking branch 'origin/main' into bettern-zen-spacing 2024-07-27 15:16:14 +02:00
Alex McLean 6e496b7a4a Fix loop at tactus (#1145)
* fix loopAt tactus
2024-07-27 11:02:38 +02:00
Jade (Rose) Rowland 71e843e1a3 Merge pull request #1130 from daslyfe/phase_vocode
Phase vocode
2024-07-25 22:37:34 -04:00
Jade (Rose) Rowland 61ba33043e Merge pull request #1132 from daslyfe/udels
Udels (MultiFrame Strudel) Revisited
2024-07-25 22:37:17 -04:00
Felix Roos 943d78f268 better spacing in zen mode 2024-07-25 16:44:02 +02:00
Felix Roos 5ac59e95dd handle midin device not found error 2024-07-25 16:07:58 +02:00
Felix Roos 03bbc0bc27 hotfix: remove log 2024-07-25 09:19:41 +02:00
Alex McLean 4c3457438d Fix tactus marking in mininotation (#1144)
* more tactus tweaks
* format
2024-07-24 10:40:28 +01:00
Jade (Rose) Rowland 94c07b6b7a prettier 2024-07-10 19:11:06 -04:00
Jade (Rose) Rowland 42000a7628 push code to window on aftereval 2024-07-09 22:48:14 -04:00
Jade (Rose) Rowland b5fbcd108f cleanup 2024-06-17 23:41:34 -04:00
Jade (Rose) Rowland 5e9da78e64 fixed window: 2024-06-14 20:42:39 -04:00
Jade (Rose) Rowland f3b83b41f5 prettier 2024-06-14 15:40:37 -04:00
Jade (Rose) Rowland 1d95da7a3a fixed build window issue: 2024-06-14 15:28:24 -04:00
Jade (Rose) Rowland 2a2ddf205a need to fix window message 2024-06-14 14:26:23 -04:00
Jade (Rose) Rowland 6c9e0aaa1a added udels editor and header 2024-06-14 01:24:08 -04:00
Jade (Rose) Rowland d23038d386 Merge branch 'tidalcycles:main' into udels 2024-06-13 22:52:06 -04:00
Jade (Rose) Rowland 248f67d1ab prettier 2024-06-12 10:28:12 -04:00
Jade (Rose) Rowland f3fa1483d7 fix pnpm lock 2024-06-12 01:14:59 -04:00
Jade (Rose) Rowland 11102adb75 unessecary import 2024-06-12 01:14:20 -04:00
Jade (Rose) Rowland 9f958abb08 fixed offset time 2024-06-12 01:11:58 -04:00
Jade (Rose) Rowland 065b24a2ee working 2024-06-10 23:51:38 -04:00
Jade (Rose) Rowland e6f3b1f1ef copied files 2024-06-10 00:06:35 -04:00
Felix Roos 69a5d62027 rename "samples" to "bank" 2024-05-17 09:42:03 +02:00
Felix Roos 226cf356d6 fix: lint 2024-05-16 11:05:38 +02:00
Felix Roos 73d49f152a refactor sampler:
- delegate getSampleBufferSource logic to getSampleInfo + getSampleBuffer
- move buffer logic up from onTriggerSample to those functions
2024-05-16 10:52:54 +02:00
Jade (Rose) Rowland 5ca9afa9e8 restored 2024-04-15 19:53:12 -04:00
Felix Roos cdba9efb59 add filter + filterWhen + within 2024-04-01 03:17:15 +02:00
65 changed files with 2843 additions and 1191 deletions
+4 -11
View File
@@ -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++;
+18 -5
View File
@@ -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.
+1 -2
View File
@@ -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);
+15 -55
View File
@@ -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) {
+144 -110
View File
@@ -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',
};
}),
),
),
);
+52 -1
View File
@@ -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();
+5
View File
@@ -1248,4 +1248,9 @@ describe('Pattern', () => {
);
});
});
describe('loopAt', () => {
it('maintains tactus', () => {
expect(s('bev').chop(8).loopAt(2).tactus).toStrictEqual(Fraction(4));
});
});
});
-8
View File
@@ -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
+73
View File
@@ -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
+28 -35
View File
@@ -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);
};
+1 -1
View File
@@ -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));
+6 -1
View File
@@ -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(
+173 -223
View File
@@ -1,253 +1,203 @@
/*
mini.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs>
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs>
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 * as krill from './krill-parser.js';
import * as strudel from '@strudel/core';
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
const randOffset = 0.0003;
const applyOptions = (parent, enter) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const ops = options?.ops;
if (ops) {
for (const op of ops) {
switch (op.type_) {
case 'stretch': {
const legalTypes = ['fast', 'slow'];
const { type, amount } = op.arguments_;
if (!legalTypes.includes(type)) {
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
}
pat = strudel.reify(pat)[type](enter(amount));
break;
}
case 'replicate': {
const { amount } = op.arguments_;
pat = strudel.reify(pat);
pat = pat._repeatCycles(amount)._fast(amount);
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
} else {
pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
}
break;
}
case 'degradeBy': {
pat = strudel
.reify(pat)
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
break;
}
case 'tail': {
const friend = enter(op.arguments_.element);
pat = pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
break;
}
case 'range': {
const friend = enter(op.arguments_.element);
pat = strudel.reify(pat);
const arrayRange = (start, stop, step = 1) =>
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
start < stop ? start + index * step : start - index * step,
);
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
pat = range(pat, friend);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
}
}
}
return pat;
// "parser"
let token_types = {
open_polyrhythm: /^\[/,
close_polyrhythm: /^\]/,
open_polymeter: /^\{/,
close_polymeter: /^\}/,
open_slowpolymeter: /^\</,
close_slowpolymeter: /^\>/,
subpat_delimiter: /^,/,
fast: /^\*/,
slow: /^\//,
// is there a better way without back-tracking for whole numbers?
// float: /^(\d*\.)?\d+/,
plain: /^[a-zA-Z0-9\.\#]+/,
};
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) {
onEnter?.(ast);
const enter = (node) => patternifyAST(node, code, onEnter, offset);
switch (ast.type_) {
case 'pattern': {
// resolveReplications(ast);
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment;
const with_tactus = children.filter((child) => child.__tactus_source);
let pat;
switch (alignment) {
case 'stack': {
pat = strudel.stack(...children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter_slowcat': {
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter': {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
let token_offset;
function next_token(code, global_offset) {
for (let type in token_types) {
const match = code.match(token_types[type]);
if (match) {
const leftpad = match[0].length - match[0].trimLeft().length;
const rightpad = match[0].length - match[0].trimRight().length;
const result = {
type,
value: match[0],
location: [token_offset + leftpad + global_offset, token_offset + match[0].length + global_offset - rightpad],
};
token_offset += match[0].length;
return result;
}
}
throw new Error('could not match "' + code + '"');
}
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight))));
pat = strudel.stack(...aligned);
break;
}
case 'rand': {
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'feet': {
pat = strudel.fastcat(...children);
break;
}
default: {
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) {
const weightSum = ast.source_.reduce(
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
strudel.Fraction(0),
);
pat = strudel.timeCat(
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
);
pat.__weight = weightSum; // for polymeter
pat.tactus = weightSum;
if (with_tactus.length) {
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
}
} else {
pat = strudel.sequence(...children);
pat.tactus = children.length;
}
if (ast.arguments_.tactus) {
pat.__tactus_source = true;
}
}
}
if (with_tactus.length) {
pat.__tactus_source = true;
}
return pat;
function tokenize(code, global_offset) {
let tokens = [];
token_offset = 0;
while (code.length > 0) {
const trimmed = code.trim();
token_offset += code.length - trimmed.length;
code = trimmed;
const token = next_token(code, global_offset);
code = code.slice(token.value.length);
tokens.push(token);
}
return tokens;
}
class Parser {
parse(code, global_offset) {
this.tokens = tokenize(code, global_offset);
const subpats = this.parse_subpats();
return { type: 'polymeter', subpats };
}
consume(type) {
const token = this.tokens.shift();
this.offset += token.length;
if (token.type !== type) {
throw new Error('expected token type ' + type + ' got ' + token.type);
}
case 'element': {
1;
return enter(ast.source_);
return token;
}
parse_step() {
let next = this.tokens[0]?.type;
if (next === 'open_polyrhythm') {
return this.parse_polyrhythm();
}
case 'atom': {
if (ast.source_ === '~' || ast.source_ === '-') {
return strudel.silence;
}
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
if (offset === -1) {
// skip location handling (used when getting leaves to avoid confusion)
return strudel.pure(value);
}
const [from, to] = getLeafLocation(code, ast, offset);
return strudel.pure(value).withLoc(from, to);
if (next === 'open_polymeter') {
return this.parse_polymeter();
}
case 'stretch':
return enter(ast.source_).slow(enter(ast.arguments_.amount));
default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return strudel.silence;
if (next === 'open_slowpolymeter') {
return this.parse_slowpolymeter();
}
if (next === 'plain') {
return this.consume('plain');
}
throw new Error('unexpected token ' + this.tokens[0]?.value + ' of type ' + this.tokens[0]?.type);
}
parse_expr() {
let result = this.parse_step();
let looking = true;
while (looking) {
let next = this.tokens[0]?.type;
if (!next) {
looking = false;
} else {
switch (next) {
case 'fast':
case 'slow':
this.consume(next);
const factor = this.parse_step();
result = { type: next, factor, step: result };
break;
default:
looking = false;
}
}
}
return result;
}
parse_seq(...close_types) {
const steps = [];
while (!close_types.includes(this.tokens[0]?.type)) {
steps.push(this.parse_expr());
}
return { type: 'seq', steps };
}
parse_subpats(close_type) {
const subpats = [];
while (true) {
subpats.push(this.parse_seq(close_type, 'subpat_delimiter'));
const next = this.tokens[0]?.type;
if (next) {
this.consume(next);
}
if (next === close_type) {
break;
}
}
return subpats;
}
parse_polymeter() {
this.consume('open_polymeter');
const subpats = this.parse_subpats('close_polymeter');
return { type: 'polymeter', subpats };
}
parse_polyrhythm() {
this.consume('open_polyrhythm');
const subpats = this.parse_subpats('close_polyrhythm');
return { type: 'polyrhythm', subpats };
}
parse_slowpolymeter() {
this.consume('open_slowpolymeter');
const subpats = this.parse_subpats('close_slowpolymeter');
return { type: 'slowpolymeter', subpats };
}
}
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
// value is expected without quotes!
const { start, end } = leaf.location_;
const actual = code?.split('').slice(start.offset, end.offset).join('');
// make sure whitespaces are not part of the highlight
const [offsetStart = 0, offsetEnd = 0] = actual
? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
: [];
return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
};
// takes quoted mini string, returns ast
export const mini2ast = (code, start = 0, userCode = code) => {
try {
return krill.parse(code);
} catch (error) {
const region = [error.location.start.offset + start, error.location.end.offset + start];
const line = userCode.slice(0, region[0]).split('\n').length;
throw new Error(`[mini] parse error at line ${line}: ${error.message}`);
function patternifyTree(tree) {
if (tree.type === 'polyrhythm') {
const args = tree.subpats.map((arg) => patternifyTree(arg));
return strudel.polyrhythm(...args);
}
if (tree.type === 'slowpolymeter') {
const args = tree.subpats.map((arg) => patternifyTree(arg));
return strudel.s_polymeterSteps(1, ...args);
}
if (tree.type === 'polymeter') {
const args = tree.subpats.map((subpat) => patternifyTree(subpat));
return strudel.s_polymeter(...args);
}
if (tree.type === 'fast') {
const step = patternifyTree(tree.step);
const factor = patternifyTree(tree.factor);
return strudel.fast(factor, step);
}
if (tree.type === 'slow') {
const step = patternifyTree(tree.step);
const factor = patternifyTree(tree.factor);
return strudel.slow(factor, step);
}
if (tree.type === 'plain') {
return strudel.pure(tree.value).withLoc(tree.location[0], tree.location[1]);
}
if (tree.type === 'seq') {
const steps = tree.steps.map((step) => patternifyTree(step));
return strudel.fastcat(...steps);
}
}
const parser = new Parser();
export const m = (code, global_offset) => {
const tree = parser.parse(code, global_offset + 1);
const pat = patternifyTree(tree);
return strudel.reify(pat);
};
// takes quoted mini string, returns all nodes that are leaves
export const getLeaves = (code, start, userCode) => {
const ast = mini2ast(code, start, userCode);
let leaves = [];
patternifyAST(
ast,
code,
(node) => {
if (node.type_ === 'atom') {
leaves.push(node);
}
},
-1,
);
return leaves;
};
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
export const getLeafLocations = (code, start = 0, userCode) => {
return getLeaves(code, start, userCode).map((l) => getLeafLocation(code, l, start));
export const getLeafLocations = (code, start) => {
code = code.replace(/^"/, '');
code = code.replace(/"$/, '');
const leaves = tokenize(code, start).filter((x) => x.type == 'plain');
return leaves.map((x) => x.location.map((y) => y + 1));
};
// mini notation only (wraps in "")
export const mini = (...strings) => {
const pats = strings.map((str) => {
const code = `"${str}"`;
const ast = mini2ast(code);
return patternifyAST(ast, code);
});
const pats = strings.map(m);
return strudel.sequence(...pats);
};
// turns str mini string (without quotes) into pattern
// offset is the position of the mini string in the JS code
// each leaf node will get .withLoc added
// this function is used by the transpiler for double quoted strings
export const m = (str, offset) => {
const code = `"${str}"`;
const ast = mini2ast(code);
return patternifyAST(ast, code, null, offset);
};
// includes haskell style (raw krill parsing)
export const h = (string) => {
const ast = mini2ast(string);
return patternifyAST(ast, string);
};
export function minify(thing) {
if (typeof thing === 'string') {
return mini(thing);
+3 -14
View File
@@ -210,25 +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));
});
});
describe('getLeafLocation', () => {
it('gets location of leaf nodes', () => {
const code = '"bd sd"';
const ast = mini2ast(code);
const bd = ast.source_[0].source_;
expect(getLeafLocation(code, bd)).toEqual([1, 3]);
const sd = ast.source_[1].source_;
expect(getLeafLocation(code, sd)).toEqual([4, 6]);
expect(mini('[^a b c d e]').tactus).toEqual(Fraction(5));
});
});
@@ -240,9 +229,9 @@ describe('getLeafLocations', () => {
]);
expect(getLeafLocations('"bd*2 [sd cp]"')).toEqual([
[1, 3], // bd columns
[4, 5], // "2" columns
[7, 9], // sd columns
[10, 12], // cp columns
[4, 5], // "2" columns
]);
expect(getLeafLocations('"bd*<2 3>"')).toEqual([
[1, 3], // bd columns
+40 -30
View File
@@ -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);
};
+10
View File
@@ -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);
};
+2 -2
View File
@@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
if (!(name in writeMessagers)) {
getWriter(name, br);
}
const onTrigger = (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);
+488
View File
@@ -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;
}
}
+185
View File
@@ -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;
+61 -48
View File
@@ -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;
+19 -4
View File
@@ -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);
};
+184
View File
@@ -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);
+6 -4
View File
@@ -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")?;
+519 -258
View File
@@ -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/423/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 ]",
+1 -1
View File
@@ -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>
);
}
+72
View File
@@ -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 { Panel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
// 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} />
<Panel 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
+4
View File
@@ -120,6 +120,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 +130,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 +142,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 +153,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" />
+1 -1
View File
@@ -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} />
+2 -2
View File
@@ -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>
-12
View File
@@ -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} />
+3 -5
View File
@@ -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 />
+12
View File
@@ -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
View File
@@ -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>
);
}
+21
View File
@@ -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 { Panel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
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' && <Panel context={context} />}
</div>
<UserFacingErrorMessage error={error} />
{panelPosition === 'bottom' && <Panel 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,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>
);
}
@@ -3,7 +3,7 @@ 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>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
@@ -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,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 }) {
@@ -4,7 +4,7 @@ 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 { setActiveFooter, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
@@ -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(' ');
@@ -99,7 +99,7 @@ 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">
@@ -1,5 +1,7 @@
import jsdocJson from '../../../../doc.json';
const visibleFunctions = jsdocJson.docs
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));
@@ -10,9 +12,29 @@ const getInnerText = (html) => {
};
export function Reference() {
const [search, setSearch] = useState('');
const visibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (!search) {
return true;
}
return entry.name.includes(search) || (entry.synonyms?.some((s) => s.includes(search)) ?? false);
});
}, [search]);
return (
<div className="flex h-full w-full pt-2 text-foreground overflow-hidden">
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
<div class="w-full ml-2 mb-2 top-0 sticky">
<input
className="w-full p-1 bg-background rounded-md"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
</div>
{visibleFunctions.map((entry, i) => (
<a
key={i}
@@ -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 && (
{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
@@ -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 React, { 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,17 @@ 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">
<div className="w-full ml-2 mb-2 top-0 sticky">
<input
className="w-full p-1 bg-background rounded-md"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup
value={soundsFilter}
+3 -3
View File
@@ -23,10 +23,10 @@ async function hasStrudelJson(subpath) {
async function loadStrudelJson(subpath) {
const contents = await readTextFile(subpath + '/strudel.json', { dir });
const sampleMap = JSON.parse(contents);
processSampleMap(sampleMap, (key, 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',
});
+239
View File
@@ -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;
}
+47 -25
View File
@@ -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
@@ -97,6 +94,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 +112,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 });
+18 -3
View File
@@ -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',
@@ -27,9 +33,18 @@ export const defaultSettings = {
panelPosition: 'right',
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);
@@ -54,9 +69,9 @@ 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,
};
}
+13 -12
View File
@@ -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();