Compare commits

..

3 Commits

Author SHA1 Message Date
Felix Roos 9b765085e0 example: using onPaint with @strudel/web 2023-07-07 08:59:36 +02:00
Felix Roos 28b978f95f can now await initStrudel to get scheduler 2023-07-07 08:57:28 +02:00
Felix Roos cdf5456b75 fix: reset pianoroll linewidth to 1 2023-07-07 08:56:47 +02:00
111 changed files with 4258 additions and 9021 deletions
+1 -2
View File
@@ -17,5 +17,4 @@ vite.config.js
**/*.ts
**/*.json
**/dev-dist
**/dist
/src-tauri/target/**/*
**/dist
-1
View File
@@ -2,7 +2,6 @@
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
+1 -1
View File
@@ -67,6 +67,6 @@
"lerna": "^6.6.1",
"prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.8.1",
"vitest": "^0.33.0"
"vitest": "^0.28.0"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "0.9.0",
"version": "0.8.4",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
+15 -412
View File
@@ -109,97 +109,6 @@ const generic_params = [
*/
['attack', 'att'],
/**
* Sets the Frequency Modulation Harmonicity Ratio.
* Controls the timbre of the sound.
* Whole numbers and simple ratios sound more natural,
* while decimal numbers and complex ratios sound metallic.
*
* @name fmh
* @param {number | Pattern} harmonicity
* @example
* note("c e g b")
* .fm(4)
* .fmh("<1 2 1.5 1.61>")
* .scope()
*
*/
[['fmh', 'fmi'], 'fmh'],
/**
* Sets the Frequency Modulation of the synth.
* Controls the modulation index, which defines the brightness of the sound.
*
* @name fm
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
* note("c e g b")
* .fm("<0 1 2 8 32>")
* .scope()
*
*/
[['fmi', 'fmh'], 'fm'],
// fm envelope
/**
* Ramp type of fm envelope. Exp might be a bit broken..
*
* @name fmenv
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.2)
* .fmsustain(0)
* .fmenv("<exp lin>")
* .scope()
*
*/
['fmenv'],
/**
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
* @param {number | Pattern} time attack time
* @example
* note("c e g b")
* .fm(4)
* .fmattack("<0 .05 .1 .2>")
* .scope()
*
*/
['fmattack'],
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
* @name fmdecay
* @param {number | Pattern} time decay time
* @example
* note("c e g b")
* .fm(4)
* .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4)
* .scope()
*
*/
['fmdecay'],
/**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
* @param {number | Pattern} level sustain level
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>")
* .scope()
*
*/
['fmsustain'],
// these are not really useful... skipping for now
['fmrelease'],
['fmvelocity'],
/**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
*
@@ -211,9 +120,6 @@ const generic_params = [
*/
['bank'],
['analyze'], // analyser node send amount 0 - 1 (used by scope)
['fft'], // fftSize of analyser
/**
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
* Note that the decay is only audible if the sustain value is lower than 1.
@@ -299,52 +205,16 @@ const generic_params = [
*/
['end'],
/**
* Loops the sample.
* Loops the sample (from `begin` to `end`) the specified number of times.
* Note that the tempo of the loop is not synced with the cycle tempo.
* To change the loop region, use loopBegin / loopEnd.
*
* @name loop
* @param {number | Pattern} on If 1, the sample is looped
* @param {number | Pattern} times How often the sample is looped
* @example
* s("casio").loop(1)
* s("bd").loop("<1 2 3 4>").osc()
*
*/
['loop'],
/**
* Begin to loop at a specific point in the sample (inbetween `begin` and `end`).
* Note that the loop point must be inbetween `begin` and `end`, and before `loopEnd`!
* Note: Samples starting with wt_ will automatically loop! (wt = wavetable)
*
* @name loopBegin
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample
* @synonyms loopb
* @example
* s("space").loop(1)
* .loopBegin("<0 .125 .25>").scope()
*/
['loopBegin', 'loopb'],
/**
*
* End the looping section at a specific point in the sample (inbetween `begin` and `end`).
* Note that the loop point must be inbetween `begin` and `end`, and after `loopBegin`!
*
* @name loopEnd
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample
* @synonyms loope
* @example
* s("space").loop(1)
* .loopEnd("<1 .75 .5 .25>").scope()
*/
['loopEnd', 'loope'],
/**
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/**
@@ -359,6 +229,15 @@ const generic_params = [
*/
// ['legato'],
// ['clhatdecay'],
/**
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
['crush'],
/**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
@@ -370,6 +249,7 @@ const generic_params = [
*
*/
['coarse'],
/**
* choose the channel the pattern is sent to in superdirt
*
@@ -403,227 +283,6 @@ const generic_params = [
*
*/
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa(.5)
* .lpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['lpenv', 'lpe'],
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa(.5)
* .hpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['hpenv', 'hpe'],
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa(.5)
* .bpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['bpenv', 'bpe'],
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa("<.5 .25 .1 .01>/4")
* .lpenv(4)
*/
['lpattack', 'lpa'],
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa("<.5 .25 .1 .01>/4")
* .hpenv(4)
*/
['hpattack', 'hpa'],
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa("<.5 .25 .1 .01>/4")
* .bpenv(4)
*/
['bpattack', 'bpa'],
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd("<.5 .25 .1 0>/4")
* .lps(0.2)
* .lpenv(4)
*/
['lpdecay', 'lpd'],
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd("<.5 .25 .1 0>/4")
* .hps(0.2)
* .hpenv(4)
*/
['hpdecay', 'hpd'],
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd("<.5 .25 .1 0>/4")
* .bps(0.2)
* .bpenv(4)
*/
['bpdecay', 'bpd'],
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd(.5)
* .lps("<0 .25 .5 1>/4")
* .lpenv(4)
*/
['lpsustain', 'lps'],
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd(.5)
* .hps("<0 .25 .5 1>/4")
* .hpenv(4)
*/
['hpsustain', 'hps'],
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd(.5)
* .bps("<0 .25 .5 1>/4")
* .bpenv(4)
*/
['bpsustain', 'bps'],
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .lpf(500)
* .lpenv(4)
* .lpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['lprelease', 'lpr'],
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .hpf(500)
* .hpenv(4)
* .hpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['hprelease', 'hpr'],
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .bpf(500)
* .bpenv(4)
* .bpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['bprelease', 'bpr'],
/**
* Sets the filter type. The 24db filter is more aggressive. More types might be added in the future.
* @name ftype
* @param {number | Pattern} type 12db (default) or 24db
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .bpenv(4)
* .ftype("<12db 24db>")
*/
['ftype'],
['fanchor'],
/**
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
*
@@ -640,36 +299,6 @@ const generic_params = [
*/
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
// ['hpf'],
/**
* Applies a vibrato to the frequency of the oscillator.
*
* @name vib
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
* note("a")
* .vib("<.5 1 2 4 8 16>")
* @example
* // change the modulation depth with ":"
* note("a")
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
/**
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
* @name vibmod
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
* note("a").vib(4)
* .vibmod("<.25 .5 1 2 12>")
* @example
* // change the vibrato frequency with ":"
* note("a")
* .vibmod("<.25 .5 1 2 12>:8")
*/
[['vibmod', 'vib'], 'vmod'],
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
/**
* Controls the **h**igh-**p**ass **q**-value.
@@ -848,9 +477,6 @@ const generic_params = [
*
*/
['lsize'],
// label for pianoroll
['activeLabel'],
[['label', 'activeLabel']],
// ['lfo'],
// ['lfocutoffint'],
// ['lfodelay'],
@@ -883,7 +509,7 @@ const generic_params = [
* @superDirtOnly
*/
['octave'],
['offset'], // TODO: what is this? not found in tidal doc
// ['ophatdecay'],
// TODO: example
/**
@@ -947,15 +573,6 @@ const generic_params = [
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['velocity'],
['voice'], // TODO: synth param
// voicings // https://github.com/tidalcycles/strudel/issues/506
['chord'], // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
['dictionary', 'dict'], // which dictionary to use for the voicings
['anchor'], // the top note to align the voicing to, defaults to c5
['offset'], // how the voicing is offset from the anchored position
['octaves'], // how many octaves are voicing steps spread apart, defaults to 1
[['mode', 'anchor']], // below = anchor note will be removed from the voicing, useful for melody harmonization
/**
* Sets the level of reverb.
*
@@ -1144,22 +761,8 @@ const generic_params = [
*
*/
['clip'],
// ZZFX
['zrand'],
['curve'],
['slide'], // superdirt duplicate
['deltaSlide'],
['pitchJump'],
['pitchJumpTime'],
['lfo', 'repeatTime'],
['noise'],
['zmod'],
['zcrush'], // like crush but scaled differently
['zdelay'],
['tremolo'],
['zzfx'],
];
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
controls.createParam = function (names) {
+6 -10
View File
@@ -9,29 +9,25 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id);
if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}, 200);
});
}
return canvas.getContext('2d');
};
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
if (typeof window === 'undefined') {
return this;
}
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
@@ -63,7 +59,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.9.0",
"version": "0.8.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@@ -36,6 +36,6 @@
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vitest": "^0.28.0"
}
}
+13 -38
View File
@@ -783,14 +783,13 @@ export class Pattern {
hap.setContext({
...hap.context,
onTrigger: (...args) => {
// run previously set trigger, if it exists
hap.context.onTrigger?.(...args);
if (!dominant && hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
onTrigger(...args);
},
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
// when using multiple triggers, you cannot flip this flag to false again!
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
dominantTrigger: hap.context.dominantTrigger || dominant,
// we need this to know later if the default trigger should still fire
dominantTrigger: dominant,
}),
);
}
@@ -2241,18 +2240,14 @@ const _loopAt = function (factor, pat, cps = 1) {
.slow(factor);
};
/**
/*
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
* @name slice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/
export const slice = register(
@@ -2263,9 +2258,9 @@ export const slice = register(
opat.outerBind((o) => {
// If it's not an object, assume it's a string and make it a 's' control parameter
o = o instanceof Object ? o : { s: o };
const begin = Array.isArray(n) ? n[i] : i / n;
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
return pure({ begin, end, _slices: n, ...o });
// Remember we must stay pure and avoid editing the object directly
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
return pure({ ...toAdd, ...o });
}),
),
);
@@ -2273,14 +2268,14 @@ export const slice = register(
false, // turns off auto-patternification
);
/**
/*
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice
* @memberof Pattern
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165")
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
* .hurry(0.65)
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
*/
export const splice = register(
@@ -2307,26 +2302,6 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
return _loopAt(factor, pat, 1);
});
// the fit function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
/**
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to loopAt.
* @name fit
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/4").fit()
*/
export const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: 1 / hap.whole.duration,
unit: 'c',
})),
),
);
/**
* Makes the sample fit the given number of cycles and cps value, by
* changing the speed. Please note that at some point cps will be
+171 -85
View File
@@ -29,26 +29,138 @@ const getValue = (e) => {
return value;
};
Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
Pattern.prototype.pianoroll = function ({
cycles = 4,
playhead = 0.5,
overscan = 1,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
labels = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
this.draw(
(ctx, haps, t) => {
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
pianoroll({
...options,
time: t,
ctx,
haps: haps.filter(inFrame),
events.filter(inFrame).forEach((event) => {
const isActive = event.whole.begin <= t && event.endClipped > t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(t / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
},
{
from: from - overscan,
to: to + overscan,
onQuery: (events) => {
const { min, max, values } = events.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
},
);
return this;
@@ -79,13 +191,6 @@ export function pianoroll({
fold = 0,
vertical = 0,
labels = false,
fill = 1,
fillActive = false,
strokeActive = true,
stroke,
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
ctx,
} = {}) {
const w = ctx.canvas.width;
@@ -93,6 +198,8 @@ export function pianoroll({
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
ctx.lineWidth = 1;
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
@@ -129,75 +236,58 @@ export function pianoroll({
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
haps.forEach((event) => {
const isActive = event.whole.begin <= time && event.endClipped > time;
let strokeCurrent = stroke ?? (strokeActive && isActive);
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
if (hideInactive && !isActive) {
return;
}
let color = event.value?.color || event.context?.color;
active = color || active;
inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valueProgress = fold
? foldValues.indexOf(value) / foldValues.length
: (Number(value) - minMidi) / valueExtent;
const valuePx = scale(valueProgress, ...valueRange);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
/* const xFactor = Math.sin(performance.now() / 500) + 1;
coords[0] *= xFactor; */
if (strokeCurrent) {
ctx.strokeRect(...coords);
}
if (fillCurrent) {
ctx.fillRect(...coords);
}
//ctx.ellipse(...ellipseFromRect(...coords))
if (labels) {
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
const { label: inactiveLabel, activeLabel } = event.value;
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
const label = customLabel ?? defaultLabel;
let measure = vertical ? durationPx : barThickness * 0.75;
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
// font color
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
/* const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
haps
// .filter(inFrame)
.forEach((event) => {
const isActive = event.whole.begin <= time && event.whole.end > time;
const color = event.value?.color || event.context?.color;
ctx.fillStyle = color || inactive;
ctx.strokeStyle = color || active;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(time / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
if (labels) {
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
ctx.font = `${barThickness * 0.75}px monospace`;
ctx.strokeStyle = 'black';
ctx.fillStyle = isActive ? 'white' : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
@@ -223,15 +313,11 @@ export function getDrawOptions(drawTime, options = {}) {
}
Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
);
};
Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
+5 -8
View File
@@ -11,24 +11,21 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
if (!pc) {
return [];
}
return [pc, acc, oct ? Number(oct) : undefined];
};
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
// turns the given note into its midi number representation
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
export const noteToMidi = (note) => {
const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/csound",
"version": "0.9.0",
"version": "0.8.0",
"description": "csound bindings for strudel",
"main": "index.mjs",
"publishConfig": {
-3
View File
@@ -1,3 +0,0 @@
# @strudel/desktopbridge
This package contains utilities used to communicate with the Tauri backend
-9
View File
@@ -1,9 +0,0 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/desktopbridge/index.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/>.
*/
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';
-11
View File
@@ -1,11 +0,0 @@
import { listen } from '@tauri-apps/api/event';
import { logger } from '../core/logger.mjs';
// listen for log events from the Tauri backend and log in the UI
await listen('log-event', (e) => {
if (e.payload == null) {
return;
}
const { message, message_type } = e.payload;
logger(message, message_type);
});
-52
View File
@@ -1,52 +0,0 @@
import { Invoke } from './utils.mjs';
import { Pattern, noteToMidi } from '@strudel.cycles/core';
const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime) => {
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
const offset = (time - currentTime) * 1000;
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1;
const requestedport = output ?? 'IAC';
const messagesfromjs = [];
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
messagesfromjs.push({
requestedport,
message: [ON_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset,
});
messagesfromjs.push({
requestedport,
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
offset: roundedOffset + duration,
});
}
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
if (!['string', 'number'].includes(typeof ccn)) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
messagesfromjs.push({
requestedport,
message: [CC_MESSAGE + midichan, ccn, scaled],
offset: roundedOffset,
});
}
// invoke is temporarily blocking, run in an async process
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendmidi', { messagesfromjs });
});
}
});
};
-43
View File
@@ -1,43 +0,0 @@
import { parseNumeral, Pattern } from '@strudel.cycles/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
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 params = [];
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
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',
});
});
const messagesfromjs = [];
if (params.length) {
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
}
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendosc', { messagesfromjs });
});
}
});
};
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@strudel/desktopbridge",
"version": "0.1.0",
"private": true,
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
"main": "index.mjs",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.4.0"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}
-4
View File
@@ -1,4 +0,0 @@
import { invoke } from '@tauri-apps/api/tauri';
export const Invoke = invoke;
export const isTauri = () => window.__TAURI_IPC__ != null;
+53 -57
View File
@@ -6,8 +6,9 @@ This program is free software: you can redistribute it and/or modify it under th
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { noteToMidi } from '@strudel.cycles/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -15,28 +16,12 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
function getMidiDeviceNamesString(outputs) {
return outputs.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
const { onReady, onConnected, onDisconnected, onEnabled } = options;
if (WebMidi.enabled) {
return;
}
const { onReady, onConnected, onDisconnected } = options;
if (!supportsMidi()) {
throw new Error('Your Browser does not support WebMIDI.');
}
WebMidi.addListener('connected', () => {
onConnected?.(WebMidi);
});
WebMidi.addListener('enabled', () => {
onEnabled?.(WebMidi);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
onDisconnected?.(WebMidi, e);
});
return new Promise((resolve, reject) => {
if (WebMidi.enabled) {
// if already enabled, just resolve WebMidi
@@ -47,6 +32,13 @@ export function enableWebMidi(options = {}) {
if (err) {
reject(err);
}
WebMidi.addListener('connected', (e) => {
onConnected?.(WebMidi);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
onDisconnected?.(WebMidi, e);
});
onReady?.(WebMidi);
resolve(WebMidi);
});
@@ -55,6 +47,8 @@ export function enableWebMidi(options = {}) {
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
let midiReady;
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
@@ -66,19 +60,29 @@ function getDevice(output, outputs) {
if (typeof output === 'string') {
return outputByName(output);
}
// attempt to default to first IAC device if none is specified
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
const device = IACOutput ?? outputs[0];
if (!device) {
throw new Error(
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
);
}
return IACOutput ?? outputs[0];
return outputs[0];
}
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output) {
if (!supportsMidi()) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
/* await */ enableWebMidi({
onConnected: ({ outputs }) =>
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
onReady: ({ outputs }) => {
const device = getDevice(output, outputs);
const otherOutputs = outputs
.filter((o) => o.name !== device.name)
.map((o) => `'${o.name}'`)
.join(' | ');
midiReady = true;
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
},
});
if (isPattern(output)) {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
@@ -86,43 +90,35 @@ Pattern.prototype.midi = function (output) {
}')`,
);
}
enableWebMidi({
onEnabled: ({ outputs }) => {
const device = getDevice(output, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) {
return this.onTrigger((time, hap) => {
if (!midiReady) {
return;
}
const device = getDevice(output, WebMidi.outputs);
if (!device) {
throw new Error(
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
.map((o) => `'${o.name}'`)
.join(' | ')}`,
);
}
hap.ensureObjectValue();
const offset = (time - currentTime) * 1000;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${offset}`;
// calculate time
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
time = time * 1000 + timingOffset;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
const duration = hap.duration.valueOf() * 1000 - 5;
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
device.playNote(midiNote, midichan, {
time: timeOffsetString,
if (note) {
const midiNumber = noteToMidi(note);
device.playNote(midiNumber, midichan, {
time,
duration,
attack: velocity,
});
}
if (ccv && ccn) {
@@ -133,7 +129,7 @@ Pattern.prototype.midi = function (output) {
throw new Error('expected ccn to be a number or a string');
}
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
device.sendControlChange(ccn, scaled, midichan, { time });
}
});
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.9.0",
"version": "0.8.0",
"description": "Midi API for strudel",
"main": "index.mjs",
"publishConfig": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.9.0",
"version": "0.8.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@@ -37,6 +37,6 @@
"devDependencies": {
"peggy": "^3.0.2",
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vitest": "^0.28.0"
}
}
+9 -6
View File
@@ -5,7 +5,6 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
let connection; // Promise<OSC>
@@ -34,6 +33,9 @@ function connect() {
return connection;
}
const latency = 0.1;
let startedAt = -1;
/**
*
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
@@ -49,16 +51,17 @@ Pattern.prototype.osc = function () {
const osc = await connect();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
if (startedAt < 0) {
startedAt = Date.now() - currentTime * 1000;
}
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 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 = (time - currentTime) * 1000;
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
const ts = Math.floor(Date.now() + offset);
const ts = Math.floor(startedAt + (time + latency) * 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/osc",
"version": "0.9.0",
"version": "0.8.0",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.9.0",
"version": "0.8.0",
"description": "React components for strudel",
"main": "src/index.js",
"publishConfig": {
+2 -2
View File
@@ -25,10 +25,10 @@ function usePatternFrame({ pattern, started, getTime, onDraw, drawTime = [-2, 2]
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
lastFrame.current = phase;
visibleHaps.current = (visibleHaps.current || [])
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame
.filter((h) => h.whole.end >= phase - lookbehind - lookahead) // in frame
.concat(haps.filter((h) => h.hasOnset()));
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
}, [pattern, onDraw]),
}, [pattern]),
);
useEffect(() => {
if (started) {
+2 -3
View File
@@ -18,7 +18,6 @@ function useStrudel({
canvasId,
drawContext,
drawTime = [-2, 2],
paintOptions = {},
}) {
const id = useMemo(() => s4(), []);
canvasId = canvasId || `canvas-${id}`;
@@ -86,9 +85,9 @@ function useStrudel({
(pattern, time, haps, drawTime) => {
const { onPaint } = pattern.context || {};
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
onPaint?.(ctx, time, haps, drawTime, paintOptions);
onPaint?.(ctx, time, haps, drawTime);
},
[drawContext, canvasId, paintOptions],
[drawContext, canvasId],
);
const drawFirstFrame = useCallback(
-1
View File
@@ -8,5 +8,4 @@ export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useKeydown } from './hooks/useKeydown';
export { default as useEvent } from './hooks/useEvent';
export { default as strudelTheme } from './themes/strudel-theme';
export { default as teletext } from './themes/teletext';
export { default as cx } from './cx';
-2
View File
@@ -12,7 +12,6 @@ export const settings = {
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'light',
@@ -36,6 +35,5 @@ export default createTheme({
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});
-1
View File
@@ -33,6 +33,5 @@ export default createTheme({
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
-1
View File
@@ -36,6 +36,5 @@ export default createTheme({
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
@@ -40,6 +40,5 @@ export default createTheme({
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});
-50
View File
@@ -1,50 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/serial",
"version": "0.9.0",
"version": "0.8.0",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"publishConfig": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.9.0",
"version": "0.8.2",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
-166
View File
@@ -1,166 +0,0 @@
# superdough
superdough is a simple web audio sampler and synth, intended for live coding.
It is the default output of [strudel](https://strudel.tidalcycles.org/).
This package has no ties to strudel and can be used to quickly bake your own music system on the web.
## Install
via npm:
```js
npm i superdough --save
```
## Use
```js
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
registerSynthSounds(),
]);
const loop = (t = 0) => {
// superdough(value, time, duration)
superdough({ s: 'bd', delay: 0.5 }, t);
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
superdough({ s: 'hh' }, t + 0.25);
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
superdough({ s: 'hh' }, t + 0.75);
};
document.getElementById('play').addEventListener('click', async () => {
await init;
let t = 0.1;
while (t < 16) {
loop(t++);
}
});
```
[Open this in Codesandbox](https://codesandbox.io/s/superdough-demo-forked-sf8djh?file=/src/index.js)
## API
### superdough(value, deadline, duration)
```js
superdough({ s: 'bd', delay: 0.5 }, 0, 1);
```
- `value`: the sound properties:
- `s`: the name of the sound as loaded via `samples` or `registerSound`
- `n`: selects sample with given index
- `bank`: prefix_ that is attached to the sound, e.g. `{ s: 'bd', bank: 'RolandTR909' }` = `{ s: 'RolandTR909_bd' }`
- `gain`: gain from 0 to 1 (higher values also work but might clip)
- `velocity`: additional gain multiplier
- `cutoff`: low pass filter cutoff
- `resonance`: low pass filter resonance
- `hcutoff`: high pass filter cutoff
- `hresonance`: high pass filter resonance
- `bandf`: band pass filter cutoff
- `bandq`: band pass filter resonance
- `crush`: amplitude bit crusher using given number of bits
- `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
- `pan`: stereo panning from 0 (left) to 1 (right)
- `vowel`: vowel filter. possible values: "a", "e", "i", "o", "u"
- `delay`: delay mix
- `delayfeedback`: delay feedback
- `delaytime`: delay time
- `room`: reverb mix
- `size`: reverb room size
- `orbit`: bus name for global effects `delay` and `room`. same orbits will get the same effects
- `freq`: repitches sound to given frequency in Hz
- `note`: repitches sound to given note or midi number
- `cut`: sets cut group. Sounds of same group will cut each other off
- `clip`: multiplies duration with given number
- `speed`: repitches sound by given factor
- `begin`: moves beginning of sample to given factor (between 0 and 1)
- `end`: moves end of sample to given factor (between 0 and 1)
- `attack`: seconds of attack phase
- `decay`: seconds of decay phase
- `sustain`: gain of sustain phase
- `release`: seconds of release phase
- `deadline`: seconds until the sound should play (0 = immediate)
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
### registerSynthSounds()
Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this:
```js
superdough({ s:'sawtooth' }, 0, 1)
```
The duration needs to be set for these sounds!
### samples(sampleMap)
allows you to load samples from URLs. There are 3 ways to load samples
1. sample map object
2. url of sample map json file
3. github repo
#### sample map object
You can pass a sample map like this:
```js
samples({
'_base': 'https://raw.githubusercontent.com/felixroos/samples/main/',
'bd': 'president/president_bd.mp3',
'sd': ['president/president_sd.mp3', 'president/president_sd2.mp3'],
'hh': ['president/president_hh.mp3'],
})
```
The `_base` property defines the root url while the others declare one or more sample paths for each sound.
For example the full URL for `bd` would then be `https://raw.githubusercontent.com/felixroos/samples/main/president/president_bd.mp3`
A loaded sound can then be played with `superdough({ s: 'bd' }, 0)`.
If you declare multiple sounds, you can select them with `n`: `superdough({ s: 'sd', n: 1 }, 0)`
The duration property is not needed for samples.
#### loading samples from a json file
Instead of passing an object as a sample map, you can also pass a URL to a json that contains a sample map:
```js
samples('https://raw.githubusercontent.com/felixroos/samples/main/strudel.json')
```
The json file is expected to have the same format as described above.
#### loading samples from a github repo
Because it is common to use github for samples, there is a short way to load a sample map from github:
```js
samples('github:tidalcycles/Dirt-Samples/master')
```
The format is `github:<user>/<repo>/<branch>`.
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
The format is also expected to be the same as explained above.
### initAudioOnFirstClick()
Initializes audio and makes sure it is playable after the first click in the document. A click is needed because of the [Autoplay Policy](https://www.w3.org/TR/autoplay-detection/).
You can call this function when the document loads.
Then just make sure your first call of `superdough` happens after a click of something.
## Credits
- [ZZFX](https://github.com/KilledByAPixel/ZzFX) used for synths starting with z
- [SuperDirt](https://github.com/musikinformatik/SuperDirt)
- [WebDirt](https://github.com/dktr0/WebDirt)
-24
View File
@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Superdough Example</title>
<meta charset="UTF-8" />
</head>
<body>
<button id="play">PLAAAAAAAY</button>
<script src="/main.js" type="module"></script>
</body>
</html>
-25
View File
@@ -1,25 +0,0 @@
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
const init = Promise.all([
initAudioOnFirstClick(),
samples('github:tidalcycles/Dirt-Samples/master'),
registerSynthSounds(),
]);
const loop = (t = 0) => {
// superdough(value, time, duration)
superdough({ s: 'bd', delay: 0.5 }, t);
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
superdough({ s: 'hh' }, t + 0.25);
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
superdough({ s: 'hh' }, t + 0.75);
};
document.getElementById('play').addEventListener('click', async () => {
await init;
let t = 0.1;
while (t < 16) {
loop(t++);
}
});
-17
View File
@@ -1,17 +0,0 @@
{
"name": "superdough-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^4.4.5"
}
}
-12
View File
@@ -1,12 +0,0 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/index.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/>.
*/
export * from './superdough.mjs';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
-7
View File
@@ -1,7 +0,0 @@
let log = (msg) => console.log(msg);
export const logger = (...args) => log(...args);
export const setLogger = (fn) => {
log = fn;
};
-41
View File
@@ -1,41 +0,0 @@
{
"name": "superdough",
"version": "0.9.8",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.cjs",
"module": "dist/index.mjs"
},
"directories": {
"example": "examples"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^4.3.3"
},
"dependencies": {
"nanostores": "^0.8.1"
}
}
-376
View File
@@ -1,376 +0,0 @@
/*
superdough.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/superdough.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 './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp } from './util.mjs';
import workletsUrl from './worklets.mjs?url';
import { createFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
export function getSound(s) {
return soundMap.get()[s];
}
export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const getAudioContext = () => {
if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext;
};
let destination;
const getDestination = () => {
const ctx = getAudioContext();
if (!destination) {
destination = ctx.createGain();
destination.connect(ctx.destination);
}
return destination;
};
export const panic = () => {
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destination = null;
};
let workletsLoading;
function loadWorklets() {
if (workletsLoading) {
return workletsLoading;
}
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
return workletsLoading;
}
function getWorklet(ac, processor, params) {
const node = new AudioWorkletNode(ac, processor);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
});
return node;
}
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const { disableWorklets = false } = options;
if (typeof window !== 'undefined') {
await getAudioContext().resume();
if (!disableWorklets) {
await loadWorklets().catch((err) => {
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
});
} else {
console.log('disableWorklets: AudioWorklet effects coarse, crush and shape are skipped!');
}
}
}
export async function initAudioOnFirstClick(options) {
return new Promise((resolve) => {
document.addEventListener('click', async function listener() {
await initAudio(options);
resolve();
document.removeEventListener('click', listener);
});
});
}
let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
dly.connect(getDestination());
delays[orbit] = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
return delays[orbit];
}
let reverbs = {};
function getReverb(orbit, duration = 2) {
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration);
reverb.connect(getDestination());
reverbs[orbit] = reverb;
}
if (reverbs[orbit].duration !== duration) {
reverbs[orbit] = reverbs[orbit].setDuration(duration);
reverbs[orbit].duration = duration;
}
return reverbs[orbit];
}
export let analyser, analyserData /* s = {} */;
export function getAnalyser(/* orbit, */ fftSize = 2048) {
if (!analyser /*s [orbit] */) {
const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize;
// getDestination().connect(analyserNode);
analyser /* s[orbit] */ = analyserNode;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
}
if (analyser /* s[orbit] */.fftSize !== fftSize) {
analyser /* s[orbit] */.fftSize = fftSize;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
}
return analyser /* s[orbit] */;
}
export function getAnalyzerData(type = 'time') {
const getter = {
time: () => analyser?.getFloatTimeDomainData(analyserData),
frequency: () => analyser?.getFloatFrequencyData(analyserData),
}[type];
if (!getter) {
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
}
getter();
return analyserData;
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
export const superdough = async (value, deadline, hapDuration) => {
const ac = getAudioContext();
if (typeof value !== 'object') {
throw new Error(
`expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`,
'error',
);
}
// duration is passed as value too..
value.duration = hapDuration;
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure
let {
s = 'triangle',
bank,
source,
gain = 0.8,
// filters
ftype = '12db',
fanchor = 0.5,
// low pass
cutoff,
lpenv,
lpattack = 0.01,
lpdecay = 0.01,
lpsustain = 1,
lprelease = 0.01,
resonance = 1,
// high pass
hpenv,
hcutoff,
hpattack = 0.01,
hpdecay = 0.01,
hpsustain = 1,
hprelease = 0.01,
hresonance = 1,
// band pass
bpenv,
bandf,
bpattack = 0.01,
bpdecay = 0.01,
bpsustain = 1,
bprelease = 0.01,
bandq = 1,
//
coarse,
crush,
shape,
pan,
vowel,
delay = 0,
delayfeedback = 0.5,
delaytime = 0.25,
orbit = 1,
room,
size = 2,
velocity = 1,
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
} = value;
gain *= velocity; // legacy fix for velocity
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) {
s = `${bank}_${s}`;
}
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, value, hapDuration);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, value, onended);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
}
} else {
throw new Error(`sound ${s} not found! Is it loaded?`);
}
if (!sourceNode) {
// if onTrigger does not return anything, we will just silently skip
// this can be used for things like speed(0) in the sampler
return;
}
if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return;
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
// gain stage
chain.push(gainNode(gain));
if (cutoff !== undefined) {
let lp = () =>
createFilter(
ac,
'lowpass',
cutoff,
resonance,
lpattack,
lpdecay,
lpsustain,
lprelease,
lpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(lp());
if (ftype === '24db') {
chain.push(lp());
}
}
if (hcutoff !== undefined) {
let hp = () =>
createFilter(
ac,
'highpass',
hcutoff,
hresonance,
hpattack,
hpdecay,
hpsustain,
hprelease,
hpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(hp());
if (ftype === '24db') {
chain.push(hp());
}
}
if (bandf !== undefined) {
let bp = () =>
createFilter(
ac,
'bandpass',
bandf,
bandq,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
}
}
if (vowel !== undefined) {
const vowelFilter = ac.createVowelFilter(vowel);
chain.push(vowelFilter);
}
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// last gain
const post = gainNode(1);
chain.push(post);
post.connect(getDestination());
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delyNode, delay);
}
// reverb
let reverbSend;
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
reverbSend = effectSend(post, reverbNode, room);
}
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, analyze);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
};
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
-181
View File
@@ -1,181 +0,0 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
};
const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = osc.frequency.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
let {
attack = 0.001,
decay = 0.05,
sustain = 0.6,
release = 0.01,
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
vib = 0,
vibmod = 0.5,
} = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({
t,
s: wave,
freq,
vib,
vibmod,
partials: n,
});
// FM + FM envelope
let stopFm, fmEnvelope;
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
}
stopFm = stop;
}
// turn down
const g = gainNode(0.3);
// gain envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
fmEnvelope?.stop(releaseTime);
let end = releaseTime + release;
stop(end);
stopFm?.(end);
},
};
},
{ type: 'synth', prebake: true },
);
});
}
export function waveformN(partials, type) {
const real = new Float32Array(partials + 1);
const imag = new Float32Array(partials + 1);
const ac = getAudioContext();
const osc = ac.createOscillator();
const amplitudes = {
sawtooth: (n) => 1 / n,
square: (n) => (n % 2 === 0 ? 0 : 1 / n),
triangle: (n) => (n % 2 === 0 ? 0 : 1 / (n * n)),
};
if (!amplitudes[type]) {
throw new Error(`unknown wave type ${type}`);
}
real[0] = 0; // dc offset
imag[0] = 0;
let n = 1;
while (n <= partials) {
real[n] = amplitudes[type](n);
imag[n] = 0;
n++;
}
const wave = ac.createPeriodicWave(real, imag);
osc.setPeriodicWave(wave);
return osc;
}
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
// Make oscillator with partial count
let o;
if (!partials || s === 'sine') {
o = getAudioContext().createOscillator();
o.type = s || 'triangle';
} else {
o = waveformN(partials, s);
}
o.frequency.value = Number(freq);
o.start(t);
// Additional oscillator for vibrato effect
let vibrato_oscillator;
if (vib > 0) {
vibrato_oscillator = getAudioContext().createOscillator();
vibrato_oscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibrato_oscillator.connect(gain);
gain.connect(o.detune);
vibrato_oscillator.start(t);
}
return {
node: o,
stop: (time) => {
vibrato_oscillator?.stop(time);
o.stop(time);
},
};
}
-53
View File
@@ -1,53 +0,0 @@
// currently duplicate with core util.mjs to skip dependency
// TODO: add separate util module?
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
return [pc, acc, oct ? Number(oct) : undefined];
};
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = chromas[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const midiToFreq = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
export const freqToMidi = (freq) => {
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
};
export const valueToMidi = (value, fallbackValue) => {
if (typeof value !== 'object') {
throw new Error('valueToMidi: expected object value');
}
let { freq, note } = value;
if (typeof freq === 'number') {
return freqToMidi(freq);
}
if (typeof note === 'string') {
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
}
if (!fallbackValue) {
throw new Error('valueToMidi: expected freq or note to be set');
}
return fallbackValue;
};
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.cjs' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
-124
View File
@@ -1,124 +0,0 @@
//import { ZZFX } from 'zzfx';
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { buildSamples } from './zzfx_fork.mjs';
export const getZZFX = (value, t) => {
let {
s,
note = 36,
freq,
//
zrand = 0,
attack = 0,
decay = 0,
sustain = 0.8,
release = 0.1,
curve = 1,
slide = 0,
deltaSlide = 0,
pitchJump = 0,
pitchJumpTime = 0,
lfo = 0,
noise = 0,
zmod = 0,
zcrush = 0,
zdelay = 0,
tremolo = 0,
duration = 0.2,
zzfx,
} = value;
const sustainTime = Math.max(duration - attack - decay, 0);
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note);
}
s = s.replace('z_', '');
const shape = ['sine', 'triangle', 'sawtooth', 'tan', 'noise'].indexOf(s) || 0;
curve = s === 'square' ? 0 : curve;
const params = zzfx || [
0.25, // volume
zrand,
freq,
attack,
sustainTime,
release,
shape,
curve,
slide,
deltaSlide,
pitchJump,
pitchJumpTime,
lfo,
noise,
zmod,
zcrush,
zdelay,
sustain, // sustain volume!
decay,
tremolo,
];
// console.log(redableZZFX(params));
const samples = /* ZZFX. */ buildSamples(...params);
const context = getAudioContext();
const buffer = context.createBuffer(1, samples.length, context.sampleRate);
buffer.getChannelData(0).set(samples);
const source = getAudioContext().createBufferSource();
source.buffer = buffer;
source.start(t);
return {
node: source,
};
};
export function registerZZFXSounds() {
['zzfx', 'z_sine', 'z_sawtooth', 'z_triangle', 'z_square', 'z_tan', 'z_noise'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
const { node: o } = getZZFX({ s: wave, ...value }, t);
o.onended = () => {
o.disconnect();
onended();
};
return {
node: o,
stop: () => {},
};
},
{ type: 'synth', prebake: true },
);
});
}
// just for debugging
function redableZZFX(params) {
const paramOrder = [
'volume',
'zrand',
'frequency',
'attack',
'sustain',
'release',
'shape',
'curve',
'slide',
'deltaSlide',
'pitchJump',
'pitchJumpTime',
'lfo',
'noise',
'zmod',
'zcrush',
'zdelay',
'sustainVolume',
'decay',
'tremolo',
];
return Object.fromEntries(paramOrder.map((param, i) => [param, params[i]]));
}
-120
View File
@@ -1,120 +0,0 @@
import { getAudioContext } from './superdough.mjs';
// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6
// changes: replaced this.volume with 1 + using sampleRate from getAudioContext()
export function buildSamples(
volume = 1,
randomness = 0.05,
frequency = 220,
attack = 0,
sustain = 0,
release = 0.1,
shape = 0,
shapeCurve = 1,
slide = 0,
deltaSlide = 0,
pitchJump = 0,
pitchJumpTime = 0,
repeatTime = 0,
noise = 0,
modulation = 0,
bitCrush = 0,
delay = 0,
sustainVolume = 1,
decay = 0,
tremolo = 0,
) {
// init parameters
let PI2 = Math.PI * 2,
sampleRate = getAudioContext().sampleRate,
sign = (v) => (v > 0 ? 1 : -1),
startSlide = (slide *= (500 * PI2) / sampleRate / sampleRate),
startFrequency = (frequency *= ((1 + randomness * 2 * Math.random() - randomness) * PI2) / sampleRate),
b = [],
t = 0,
tm = 0,
i = 0,
j = 1,
r = 0,
c = 0,
s = 0,
f,
length;
// scale by sample rate
attack = attack * sampleRate + 9; // minimum attack to prevent pop
decay *= sampleRate;
sustain *= sampleRate;
release *= sampleRate;
delay *= sampleRate;
deltaSlide *= (500 * PI2) / sampleRate ** 3;
modulation *= PI2 / sampleRate;
pitchJump *= PI2 / sampleRate;
pitchJumpTime *= sampleRate;
repeatTime = (repeatTime * sampleRate) | 0;
// generate waveform
for (length = (attack + decay + sustain + release + delay) | 0; i < length; b[i++] = s) {
if (!(++c % ((bitCrush * 100) | 0))) {
// bit crush
s = shape
? shape > 1
? shape > 2
? shape > 3 // wave shape
? Math.sin((t % PI2) ** 3) // 4 noise
: Math.max(Math.min(Math.tan(t), 1), -1) // 3 tan
: 1 - (((((2 * t) / PI2) % 2) + 2) % 2) // 2 saw
: 1 - 4 * Math.abs(Math.round(t / PI2) - t / PI2) // 1 triangle
: Math.sin(t); // 0 sin
s =
(repeatTime
? 1 - tremolo + tremolo * Math.sin((PI2 * i) / repeatTime) // tremolo
: 1) *
sign(s) *
Math.abs(s) ** shapeCurve * // curve 0=square, 2=pointy
volume *
1 * // envelope
(i < attack
? i / attack // attack
: i < attack + decay // decay
? 1 - ((i - attack) / decay) * (1 - sustainVolume) // decay falloff
: i < attack + decay + sustain // sustain
? sustainVolume // sustain volume
: i < length - delay // release
? ((length - i - delay) / release) * // release falloff
sustainVolume // release volume
: 0); // post release
s = delay
? s / 2 +
(delay > i
? 0 // delay
: ((i < length - delay ? 1 : (length - i) / delay) * // release delay
b[(i - delay) | 0]) /
2)
: s; // sample delay
}
f =
(frequency += slide += deltaSlide) * // frequency
Math.cos(modulation * tm++); // modulation
t += f - f * noise * (1 - (((Math.sin(i) + 1) * 1e9) % 2)); // noise
if (j && ++j > pitchJumpTime) {
// pitch jump
frequency += pitchJump; // apply pitch jump
startFrequency += pitchJump; // also apply to start
j = 0; // stop pitch jump time
}
if (repeatTime && !(++r % repeatTime)) {
// repeat
frequency = startFrequency; // reset frequency
slide = startSlide; // reset slide
j ||= 1; // reset pitch jump time
}
}
return b;
}
-2
View File
@@ -3,5 +3,3 @@ import './voicings.mjs';
export * from './tonal.mjs';
export * from './voicings.mjs';
import './ireal.mjs';
-523
View File
@@ -1,523 +0,0 @@
// explore them here: https://codesandbox.io/s/voicing-explorer-ireal-47tkx5?file=/src/ireal.js:0-16036
// scraped via: https://codesandbox.io/s/ireal-midi-scraper-2-gjz2mr?file=/src/index.js
export const simple = {
2: ['1P 5P 8P 9M', '1P 5P 8P 9M 12P', '5P 8P 9M 12P'],
5: ['1P 5P 8P 12P', '5P 8P 12P 15P'],
6: ['1P 5P 6M 8P 10M', '1P 5P 8P 10M 13M', '3M 5P 8P 10M 13M', '5P 8P 10M 12P 13M'],
7: [
'1P 5P 7m 8P 10M',
'1P 7m 8P 10M 12P',
'3M 7m 8P 10M 12P',
'3M 7m 8P 10M 14m',
'3M 7m 10M 12P 15P',
'7m 10M 12P 14m 15P',
'7m 10M 12P 15P 17M',
],
9: [
'1P 5P 7m 9M 10M',
'1P 7m 9M 10M 12P',
'3M 7m 8P 9M 12P',
'7m 9M 10M 14m 15P',
'3M 7m 8P 12P 16M',
'7m 10M 12P 15P 16M',
],
11: ['1P 5P 7m 9M 11P', '5P 7m 8P 9M 11P', '7m 8P 9M 11P 12P', '7m 8P 11P 12P 16M'],
13: ['1P 6M 7m 9M 10M', '1P 7m 9M 10M 13M', '3M 7m 8P 9M 13M', '7m 8P 9M 10M 13M', '7m 9M 10M 13M 15P'],
69: ['1P 5P 6M 9M 10M', '1P 5P 9M 10M 13M', '3M 5P 8P 9M 13M', '5P 8P 9M 10M 13M'],
add9: ['1P 5P 8P 9M 10M', '1P 5P 9M 10M 12P', '3M 8P 9M 10M 12P', '3M 8P 9M 12P 15P', '5P 8P 9M 12P 17M'],
'+': [
'1P 3M 6m 8P 10M',
'1P 6m 8P 10M 13m',
'3M 6m 8P 10M 13m',
'3M 8P 10M 13m 15P',
'6m 8P 10M 13m 15P',
'6m 10M 13m 15P 17M',
],
o: ['1P 5d 8P 10m 12d', '3m 8P 10m 12d 15P', '5d 8P 10m 12d 15P'],
h: [
'3m 5d 7m 8P 10m',
'1P 5d 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 7m 8P 10m 14m',
'5d 8P 10m 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
sus: ['1P 4P 5P 8P', '1P 4P 5P 8P 11P', '5P 8P 11P 12P', '5P 8P 11P 12P 15P'],
'^': ['1P 5P 8P 10M', '1P 5P 8P 10M 12P', '3M 5P 8P 10M 12P', '3M 8P 10M 12P 15P', '5P 8P 10M 12P 15P'],
'-': ['1P 3m 5P 8P 10m', '1P 5P 8P 10m 12P', '3m 5P 8P 10m 12P', '5P 8P 10m 12P 15P'],
'^7': ['1P 5P 7M 10M 12P', '1P 10M 12P 14M', '3M 8P 10M 12P 14M', '5P 8P 10M 12P 14M', '5P 8P 10M 14M 17M'],
'-7': [
'1P 3m 5P 7m 10m',
'1P 5P 7m 10m 12P',
'3m 7m 8P 10m 12P',
'3m 7m 8P 10m 14m',
'5P 7m 8P 10m 14m',
'7m 10m 12P 14m 15P',
'5P 8P 10m 14m 17m',
'7m 10m 12P 15P 17m',
],
'7sus': ['1P 5P 7m 8P 11P', '5P 8P 11P 12P 14m', '7m 8P 11P 12P 14m', '7m 11P 12P 14m 18P'],
h7: [
'3m 5d 7m 8P 10m',
'1P 5d 7m 10m 12d',
'1P 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 7m 8P 10m 14m',
'5d 8P 10m 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
o7: [
'1P 6M 8P 10m 12d',
'1P 6M 10m 12d 13M',
'3m 8P 10m 12d 13M',
'3m 8P 12d 13M 15P',
'5d 10m 12d 13M 15P',
'5d 10m 13M 15P 17m',
'6M 12d 13M 15P 17m',
'6M 12d 15P 17m 19d',
],
'^9': [
'1P 5P 7M 9M 10M',
'1P 7M 9M 10M 12P',
'3M 7M 8P 9M 12P',
'3M 7M 8P 12P 16M',
'5P 8P 10M 14M 16M',
'7M 8P 10M 12P 16M',
],
'^13': ['1P 6M 7M 9M 10M', '1P 7M 9M 10M 13M', '3M 7M 8P 9M 13M', '3M 7M 8P 13M 16M', '7M 8P 10M 13M 16M'],
'^7#11': ['1P 5P 7M 10M 12d', '3M 7M 8P 10M 12d', '1P 7M 10M 12d 14M', '3M 7M 8P 12d 14M', '5P 8P 10M 12d 14M'],
'^9#11': ['1P 3M 5d 7M 9M', '1P 7M 9M 10M 12d', '3M 7M 8P 9M 12d', '3M 8P 9M 12d 14M'],
'^7#5': ['1P 6m 7M 10M 13m', '3M 7M 8P 10M 13m', '6m 7M 8P 10M 13m'],
'-6': [
'1P 3m 5P 6M 8P',
'1P 5P 6M 8P 10m',
'3m 5P 6M 8P 10m',
'1P 5P 8P 10m 13M',
'3m 5P 8P 10m 13M',
'5P 8P 10m 12P 13M',
'5P 8P 10m 13M 15P',
],
'-69': [
'1P 3m 5P 6M 9M',
'3m 5P 6M 8P 9M',
'3m 6M 9M 10m 12P',
'1P 5P 9M 10m 13M',
'3m 5P 8P 9M 13M',
'5P 8P 9M 10m 13M',
'5P 8P 10m 13M 16M',
],
'-^7': ['1P 3m 5P 7M 10m', '1P 5P 7M 10m 12P', '3m 7M 8P 10m 12P', '5P 7M 8P 10m 14M', '5P 8P 10m 14M 17m'],
'-^9': ['1P 3m 5P 7M 9M', '1P 7M 9M 10m 12P', '3m 7M 8P 9M 12P', '5P 8P 9M 10m 14M'],
'-9': [
'1P 3m 5P 7m 9M',
'3m 5P 7m 8P 9M',
'3m 7m 8P 9M 12P',
'5P 8P 9M 10m 14m',
'3m 7m 9M 12P 15P',
'7m 10m 12P 15P 16M',
],
'-add9': ['1P 2M 3m 5P 8P', '1P 3m 5P 9M', '3m 5P 8P 9M 12P', '5P 8P 9M 10m 12P'],
'-11': [
'1P 3m 7m 9M 11P',
'3m 7m 8P 9M 11P',
'1P 4P 7m 10m 12P',
'5P 8P 11P 14m',
'3m 7m 9M 11P 15P',
'5P 8P 11P 14m 16M',
'7m 10m 12P 15P 18P',
],
'-7b5': [
'3m 5d 7m 8P 10m',
'1P 7m 10m 12d',
'1P 5d 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 7m 8P 10m 14m',
'5d 8P 10m 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
h9: ['1P 7m 9M 10m 12d', '3m 7m 8P 9M 12d', '5d 8P 9M 10m 14m', '7m 10m 12d 15P 16M'],
'-b6': ['1P 5P 6m 8P 10m', '1P 5P 8P 10m 13m', '3m 5P 8P 10m 13m', '5P 8P 10m 13m', '5P 8P 10m 13m 15P'],
'-#5': ['1P 6m 8P 10m 13m', '3m 6m 8P 10m 13m', '6m 8P 10m 13m 15P'],
'7b9': ['1P 3M 7m 9m 10M', '3M 7m 8P 9m 10M', '3M 7m 8P 9m 14m', '7m 9m 10M 14m 15P'],
'7#9': ['1P 3M 7m 10m', '3M 7m 8P 10m 14m', '7m 10m 10M 14m 15P'],
'7#11': ['1P 3M 7m 10M 12d', '3M 7m 8P 10M 12d', '7m 10M 12d 14m 15P'],
'7b5': ['1P 3M 7m 10M 12d', '3M 7m 8P 10M 12d', '7m 10M 12d 14m 15P'],
'7#5': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P'],
'9#11': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9b5': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9#5': ['1P 7m 9M 10M 13m', '3M 7m 9M 10M 13m', '3M 7m 9M 13m 14m', '7m 10M 13m 14m 16M', '7m 10M 13m 16M 17M'],
'7b13': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P'],
'7#9#5': ['1P 3M 7m 10m 13m', '3M 7m 10m 13m 15P', '7m 10M 13m 15P 17m'],
'7#9b5': ['1P 3M 7m 10m 12d', '3M 7m 10m 12d 15P', '7m 10M 12d 15P 17m'],
'7#9#11': ['1P 3M 7m 10m 12d', '3M 7m 10m 12d 15P', '7m 10M 12d 15P 17m'],
'7b9#11': ['1P 7m 9m 10M 12d', '3M 7m 8P 9m 12d', '7m 8P 10M 12d 16m'],
'7b9b5': ['1P 7m 9m 10M 12d', '3M 7m 8P 9m 12d', '7m 8P 10M 12d 16m'],
'7b9#5': ['1P 7m 9m 10M 13m', '3M 7m 8P 9m 13m', '7m 9m 10M 13m 15P'],
'7b9#9': ['1P 3M 7m 9m 10m', '3M 7m 8P 9m 10m', '7m 8P 10M 16m 17m'],
'7b9b13': ['1P 7m 9m 10M 13m', '3M 7m 8P 9m 13m', '7m 9m 10M 13m 15P'],
'7alt': [
'3M 7m 8P 9m 12d',
'1P 7m 10m 10M 13m',
'3M 7m 8P 10m 13m',
'3M 7m 9m 12d 15P',
'3M 7m 10m 13m 15P',
'7m 10M 12d 15P 17m',
'7m 10M 13m 15P 17m',
],
'13#11': ['1P 6M 7m 10M 12d', '3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'13b9': ['1P 3M 6M 7m 9m', '1P 6M 7m 9m 10M', '3M 7m 9m 10M 13M', '3M 7m 10M 13M 16m', '7m 10M 13M 16m 17M'],
'13#9': ['1P 3M 6M 7m 10m', '3M 7m 8P 10m 13M', '7m 10M 13M 14m 17m'],
'7b9sus': ['1P 5P 7m 9m 11P', '5P 7m 8P 9m 11P', '7m 8P 11P 14m 16m'],
'7susadd3': ['1P 4P 5P 7m 10M', '5P 8P 10M 11P 14m', '7m 11P 12P 15P 17M'],
'9sus': ['1P 5P 7m 9M 11P', '5P 7m 8P 9M 11P', '7m 8P 9M 11P 12P', '7m 8P 11P 12P 16M'],
'13sus': ['1P 4P 6M 7m 9M', '1P 7m 9M 11P 13M', '5P 7m 9M 11P 13M', '7m 9M 11P 13M 15P'],
'7b13sus': ['1P 5P 7m 11P 13m', '5P 7m 8P 11P 13m', '7m 11P 13m 14m 15P'],
};
export const complex = {
2: ['1P 5P 6M 8P 9M', '1P 5P 8P 9M 12P', '5P 8P 9M 12P 13M', '5P 8P 9M 12P 15P'],
5: ['1P 5P 8P 12P', '1P 5P 8P 9M 12P', '5P 8P 12P 15P', '5P 8P 12P 15P 16M'],
6: ['1P 5P 6M 9M 10M', '1P 5P 9M 10M 13M', '3M 5P 9M 10M 13M', '5P 8P 9M 10M 13M', '3M 6M 9M 12P 15P'],
7: [
'1P 5P 7m 8P 10M',
'1P 7m 8P 10M 12P',
'3M 7m 8P 10M 12P',
'3M 7m 8P 10M 14m',
'3M 7m 10M 12P 15P',
'7m 10M 12P 14m 15P',
'7m 10M 12P 15P 17M',
'7m 10M 14m 17M 19P',
],
9: [
'1P 6M 7m 9M 10M',
'3M 7m 9M 10M 12P',
'1P 7m 9M 10M 13M',
'3M 7m 9M 10M 13M',
'3M 7m 9M 12P 15P',
'7m 10M 12P 13M 16M',
'7m 10M 13M 16M 17M',
'7m 10M 13M 16M 19P',
],
11: [
'1P 4P 6M 7m 9M',
'1P 5P 7m 9M 11P',
'4P 6M 7m 9M 11P',
'5P 8P 9M 11P 14m',
'7m 9M 11P 13M 15P',
'7m 11P 12P 14m 18P',
],
13: [
'3M 7m 9M 10M 13M',
'3M 7m 9M 13M 15P',
'3M 7m 10M 13M 16M',
'7m 10M 12P 13M 16M',
'7m 10M 13M 16M 17M',
'7m 10M 13M 16M 19P',
],
69: ['1P 5P 6M 9M 10M', '1P 5P 9M 10M 13M', '3M 5P 9M 10M 13M', '5P 8P 9M 10M 13M', '3M 6M 9M 12P 15P'],
add9: [
'1P 5P 8P 9M 10M',
'1P 5P 9M 10M 12P',
'3M 8P 9M 10M 12P',
'3M 8P 9M 12P 15P',
'5P 8P 9M 10M 15P',
'5P 8P 9M 12P 17M',
],
'+': [
'1P 6m 8P 9M 10M',
'1P 6m 8P 10M 13m',
'3M 8P 9M 10M 13m',
'3M 8P 10M 13m 15P',
'6m 10M 13m 15P 16M',
'6m 10M 13m 15P 17M',
],
o: [
'1P 6M 8P 10m 12d',
'1P 6M 10m 12d 13M',
'3m 8P 10m 12d 13M',
'3m 8P 12d 13M 15P',
'5d 10m 12d 13M 15P',
'5d 10m 13M 15P 17m',
'6M 12d 13M 15P 17m',
'6M 12d 15P 17m 19d',
],
h: [
'1P 5d 7m 10m 11P',
'3m 5d 7m 8P 11P',
'5d 7m 8P 10m 11P',
'1P 7m 10m 12d',
'3m 7m 8P 12d 14m',
'5d 8P 10m 11P 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
sus: [
'1P 4P 5P 8P 9M',
'1P 4P 5P 8P 11P',
'1P 5P 8P 9M 11P',
'5P 8P 9M 11P 12P',
'5P 8P 11P 12P 13M',
'5P 8P 11P 13M 15P',
],
'^': [
'1P 3M 5P 6M 9M',
'1P 5P 8P 10M 12P',
'3M 5P 9M 10M 12P',
'1P 5P 8P 10M 13M',
'3M 8P 10M 13M 15P',
'5P 9M 10M 12P 15P',
],
'-': [
'1P 3m 5P 8P 10m',
'1P 3m 5P 9M 11P',
'3m 5P 8P 9M 11P',
'5P 8P 9M 10m 11P',
'1P 5P 9M 10m 12P',
'3m 5P 8P 10m 12P',
'5P 8P 10m 12P 15P',
],
'^7': [
'1P 6M 7M 9M 10M',
'3M 7M 9M 10M 12P',
'1P 7M 9M 10M 13M',
'3M 7M 9M 10M 13M',
'3M 7M 9M 12P 13M',
'3M 7M 9M 13M 14M',
'3M 7M 10M 13M 16M',
'7M 10M 13M 14M 16M',
'7M 10M 13M 16M 17M',
'7M 10M 13M 16M 19P',
],
'-7': [
'1P 3m 5P 7m 9M',
'1P 3m 5P 7m 10m',
'1P 5P 7m 10m 11P',
'3m 7m 8P 10m 11P',
'1P 5P 7m 10m 12P',
'3m 7m 9M 10m 12P',
'3m 7m 8P 10m 14m',
'5P 7m 9M 10m 14m',
'7m 10m 11P 14m 15P',
'7m 10m 12P 15P 16M',
'5P 8P 11P 14m 17m',
'7m 10m 12P 15P 17m',
],
'7sus': [
'1P 4P 6M 7m 9M',
'1P 5P 7m 9M 11P',
'4P 6M 7m 9M 11P',
'5P 8P 9M 11P 14m',
'7m 9M 11P 13M 15P',
'7m 11P 12P 14m 18P',
],
h7: [
'1P 5d 7m 10m 11P',
'3m 5d 7m 8P 11P',
'5d 7m 8P 10m 11P',
'1P 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 8P 10m 11P 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
o7: [
'1P 6M 8P 10m 12d',
'1P 6M 10m 12d 13M',
'3m 8P 10m 12d 13M',
'3m 8P 12d 13M 15P',
'5d 10m 12d 13M 15P',
'5d 10m 13M 15P 17m',
'6M 12d 13M 15P 17m',
'6M 12d 15P 17m 19d',
],
'^9': [
'1P 6M 7M 9M 10M',
'1P 7M 9M 10M 13M',
'3M 7M 9M 10M 13M',
'3M 7M 9M 12P 13M',
'3M 7M 8P 9M 13M',
'3M 7M 9M 13M 14M',
'3M 7M 10M 13M 16M',
'7M 10M 13M 14M 16M',
'7M 10M 13M 16M 17M',
'7M 10M 13M 16M 19P',
],
'^13': [
'1P 6M 7M 9M 10M',
'1P 7M 9M 10M 13M',
'3M 7M 9M 12P 13M',
'3M 7M 9M 10M 13M',
'3M 7M 8P 9M 13M',
'3M 7M 9M 13M 14M',
'3M 7M 10M 13M 16M',
'7M 10M 13M 14M 16M',
'7M 10M 13M 16M 17M',
'7M 10M 13M 16M 19P',
],
'^7#11': [
'1P 3M 5d 7M 9M',
'1P 7M 9M 10M 12d',
'3M 7M 9M 10M 12d',
'3M 7M 9M 12d 13M',
'3M 7M 10M 12d 14M',
'7M 10M 12d 13M 14M',
'7M 10M 12d 13M 16M',
'7M 10M 12d 14M 17M',
],
'^9#11': [
'1P 3M 5d 7M 9M',
'1P 7M 9M 10M 12d',
'3M 7M 9M 10M 12d',
'3M 7M 9M 12d 13M',
'3M 7M 9M 12d 14M',
'7M 10M 12d 14M 16M',
'7M 10M 12d 13M 16M',
],
'^7#5': ['1P 6m 7M 10M 13m', '3M 7M 9M 10M 13m', '3M 7M 10M 13m 14M', '7M 10M 13m 14M 16M', '7M 10M 13m 14M 17M'],
'-6': [
'1P 3m 5P 6M 9M',
'3m 5P 6M 8P 9M',
'1P 5P 6M 10m 11P',
'3m 5P 6M 8P 11P',
'1P 5P 9M 10m 13M',
'3m 5P 8P 9M 13M',
'5P 8P 10m 11P 13M',
'5P 8P 10m 13M 16M',
],
'-69': [
'1P 3m 5P 6M 9M',
'3m 5P 6M 8P 9M',
'3m 6M 9M 10m 12P',
'1P 5P 9M 10m 13M',
'3m 5P 8P 9M 13M',
'5P 8P 9M 10m 13M',
'5P 8P 10m 13M 16M',
],
'-^7': [
'1P 3m 5P 7M 9M',
'1P 5P 7M 10m 11P',
'3m 7M 9M 10m 11P',
'3m 7M 9M 10m 12P',
'3m 7M 9M 12P 14M',
'7M 10m 11P 12P 14M',
'7M 10m 12P 14M 16M',
],
'-^9': [
'1P 3m 5P 7M 9M',
'1P 5P 7M 10m 11P',
'3m 7M 9M 10m 11P',
'3m 7M 9M 10m 12P',
'3m 7M 9M 12P 14M',
'7M 10m 11P 12P 14M',
'7M 10m 12P 14M 16M',
],
'-9': [
'1P 3m 5P 7m 9M',
'1P 3m 7m 9M 11P',
'3m 7m 9M 10m 11P',
'3m 7m 9M 10m 12P',
'3m 7m 9M 10m 14m',
'3m 7m 9M 12P 15P',
'7m 10m 11P 14m 16M',
'7m 10m 12P 16M 18P',
],
'-add9': ['1P 2M 3m 5P 8P', '1P 3m 5P 9M', '3m 5P 8P 9M 12P', '5P 8P 9M 10m 12P'],
'-11': [
'3m 5P 7m 9M 11P',
'7m 9M 10m 11P',
'1P 4P 7m 10m 12P',
'3m 7m 9M 11P 12P',
'7m 9M 10m 11P 12P',
'3m 7m 9M 11P 14m',
'4P 10m 12P 14m',
'5P 8P 11P 14m',
'5P 8P 11P 14m 16M',
'7m 10m 12P 16M 18P',
'7m 10m 11P 16M 21m',
],
'-7b5': [
'1P 5d 7m 10m 11P',
'3m 5d 7m 8P 11P',
'5d 7m 8P 10m 11P',
'1P 7m 10m 12d',
'3m 7m 8P 10m 12d',
'3m 7m 8P 12d 14m',
'5d 8P 10m 11P 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 15P',
'5d 8P 10m 14m 17m',
],
h9: [
'3m 5d 7m 9M 11P',
'1P 7m 9M 10m 12d',
'3m 7m 9M 12d 14m',
'5d 8P 9M 10m 14m',
'7m 10m 11P 12d 14m',
'7m 10m 12d 14m 16M',
],
'-b6': ['1P 3m 5P 6m 8P', '3m 5P 8P 11P 13m', '5P 8P 10m 11P 13m'],
'-#5': ['1P 6m 8P 10m 13m', '3m 6m 8P 11P 13m', '6m 8P 10m 13m 15P'],
'7b9': ['1P 3M 7m 9m 10M', '3M 7m 8P 9m 10M', '3M 7m 8P 9m 14m', '7m 9m 10M 14m 15P'],
'7#9': ['1P 3M 7m 10m', '3M 7m 10m 10M 12P', '3M 7m 10m 12P 14m', '7m 10M 12P 14m 17m'],
'7#11': ['1P 3M 7m 9M 12d', '3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'7b5': ['1P 3M 7m 9M 12d', '3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'7#5': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P', '7m 10M 13m 14m 17M'],
'9#11': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9b5': ['1P 7m 9M 10M 12d', '3M 7m 8P 9M 12d', '7m 10M 12d 15P 16M'],
'9#5': ['1P 7m 9M 10M 13m', '3M 7m 9M 10M 13m', '3M 7m 9M 13m 14m', '7m 10M 13m 14m 16M', '7m 10M 13m 16M 17M'],
'7b13': ['1P 3M 7m 10M 13m', '3M 7m 8P 10M 13m', '3M 7m 8P 13m 14m', '7m 10M 13m 14m 15P', '7m 10M 13m 14m 17M'],
'7#9#5': ['3M 7m 10m 10M 13m', '3M 7m 10m 13m 14m', '7m 10M 13m 14m 17m'],
'7#9b5': ['3M 7m 10m 10M 12d', '3M 7m 10m 12d 14m', '7m 10M 12d 14m 17m'],
'7#9#11': ['3M 7m 10m 10M 12d', '3M 7m 10m 12d 14m', '7m 10M 12d 14m 17m'],
'7b9#11': ['3M 7m 9m 10M 12d', '3M 7m 9m 12d 14m', '7m 8P 10M 12d 16m', '7m 10M 12d 14m 16m'],
'7b9b5': ['3M 7m 9m 10M 12d', '3M 7m 9m 12d 14m', '7m 8P 10M 12d 16m', '7m 10M 12d 14m 16m'],
'7b9#5': ['1P 7m 9m 10M 13m', '3M 7m 9m 10M 13m', '3M 7m 10M 13m 16m', '7m 10M 13m 14m 16m', '7m 10M 13m 16m 17M'],
'7b9#9': ['1P 3M 7m 9m 10m', '3M 7m 10m 13m 16m', '7m 10M 13m 16m 17m'],
'7b9b13': ['1P 7m 9m 10M 13m', '3M 7m 9m 10M 13m', '3M 7m 10M 13m 16m', '7m 10M 13m 14m 16m', '7m 10M 13m 16m 17M'],
'7alt': [
'3M 7m 8P 10m 13m',
'3M 7m 9m 12d 13m',
'3M 7m 9m 10m 13m',
'3M 7m 10m 13m 14m',
'3M 7m 9m 12d 14m',
'3M 7m 10m 13m 15P',
'3M 7m 10m 13m 16m',
'7m 10M 12d 14m 16m',
'7m 10M 12d 13m 16m',
'7m 10M 13m 15P 17m',
'7m 10M 13m 16m 17m',
'7m 10M 13m 16m 19d',
],
'13#11': ['3M 7m 9M 12d 13M', '7m 10M 12d 13M 16M'],
'13b9': ['3M 7m 9m 10M 13M', '3M 7m 10M 13M 16m', '7m 10M 13M 16m 17M'],
'13#9': ['3M 7m 10m 10M 13M', '7m 10M 13M 14m 17m'],
'7b9sus': ['1P 5P 7m 9m 11P', '5P 7m 8P 9m 11P', '7m 8P 11P 14m 16m'],
'7susadd3': ['1P 4P 5P 7m 10M', '5P 8P 10M 11P 14m', '7m 11P 12P 15P 17M'],
'9sus': [
'1P 4P 6M 7m 9M',
'1P 5P 7m 9M 11P',
'4P 6M 7m 9M 11P',
'5P 8P 9M 11P 14m',
'7m 9M 11P 13M 15P',
'7m 11P 12P 14m 18P',
],
'13sus': [
'1P 4P 6M 7m 9M',
'1P 7m 9M 11P 13M',
'4P 7m 9M 11P 13M',
'7m 9M 11P 13M 15P',
'7m 11P 13M 14m 16M',
'7m 11P 13M 16M 18P',
],
'7b13sus': ['1P 5P 7m 11P 13m', '5P 7m 8P 11P 13m', '7m 11P 13m 14m 15P'],
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.9.0",
"version": "0.8.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -38,6 +38,6 @@
},
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vitest": "^0.28.0"
}
}
-152
View File
@@ -1,152 +0,0 @@
/*
tonleiter.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonleiter.test.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 { describe, test, expect } from 'vitest';
import {
Step,
Note,
transpose,
pc2chroma,
rotateChroma,
chroma2pc,
tokenizeChord,
note2pc,
note2oct,
midi2note,
renderVoicing,
scaleStep,
} from '../tonleiter.mjs';
describe('tonleiter', () => {
test('Step ', () => {
expect(Step.tokenize('#11')).toEqual(['#', 11]);
expect(Step.tokenize('b13')).toEqual(['b', 13]);
expect(Step.tokenize('bb6')).toEqual(['bb', 6]);
expect(Step.tokenize('b3')).toEqual(['b', 3]);
expect(Step.tokenize('3')).toEqual(['', 3]);
expect(Step.tokenize('10')).toEqual(['', 10]);
// expect(Step.tokenize('asdasd')).toThrow();
expect(Step.accidentals('b3')).toEqual(-1);
expect(Step.accidentals('#11')).toEqual(1);
});
test('Note', () => {
expect(Note.tokenize('C##')).toEqual(['C', '##']);
expect(Note.tokenize('Bb')).toEqual(['B', 'b']);
expect(Note.accidentals('C#')).toEqual(1);
expect(Note.accidentals('C##')).toEqual(2);
expect(Note.accidentals('Eb')).toEqual(-1);
expect(Note.accidentals('Bbb')).toEqual(-2);
});
test('transpose', () => {
expect(transpose('F#', '3')).toEqual('A#');
expect(transpose('C', '3')).toEqual('E');
expect(transpose('D', '3')).toEqual('F#');
expect(transpose('E', '3')).toEqual('G#');
expect(transpose('Eb', '3')).toEqual('G');
expect(transpose('Ebb', '3')).toEqual('Gb');
});
test('pc2chroma', () => {
expect(pc2chroma('C')).toBe(0);
expect(pc2chroma('C#')).toBe(1);
expect(pc2chroma('C##')).toBe(2);
expect(pc2chroma('D')).toBe(2);
expect(pc2chroma('Db')).toBe(1);
expect(pc2chroma('Dbb')).toBe(0);
expect(pc2chroma('bb')).toBe(10);
expect(pc2chroma('f')).toBe(5);
expect(pc2chroma('c')).toBe(0);
});
test('rotateChroma', () => {
expect(rotateChroma(0, 1)).toBe(1);
expect(rotateChroma(0, -1)).toBe(11);
expect(rotateChroma(11, 1)).toBe(0);
expect(rotateChroma(11, 13)).toBe(0);
});
test('chroma2pc', () => {
expect(chroma2pc(0)).toBe('C');
expect(chroma2pc(1)).toBe('Db');
expect(chroma2pc(1, true)).toBe('C#');
expect(chroma2pc(2)).toBe('D');
expect(chroma2pc(3)).toBe('Eb');
});
test('tokenizeChord', () => {
expect(tokenizeChord('Cm7')).toEqual(['C', 'm7', undefined]);
expect(tokenizeChord('C#m7')).toEqual(['C#', 'm7', undefined]);
expect(tokenizeChord('Bb^7')).toEqual(['Bb', '^7', undefined]);
expect(tokenizeChord('Bb^7/F')).toEqual(['Bb', '^7', 'F']);
});
test('note2pc', () => {
expect(note2pc('C5')).toBe('C');
expect(note2pc('C52')).toBe('C');
expect(note2pc('Bb3')).toBe('Bb');
expect(note2pc('F')).toBe('F');
});
test('note2oct', () => {
expect(note2oct('C5')).toBe(5);
expect(note2oct('Bb3')).toBe(3);
expect(note2oct('C7')).toBe(7);
expect(note2oct('C10')).toBe(10);
});
test('midi2note', () => {
expect(midi2note(60)).toBe('C4');
expect(midi2note(61)).toBe('Db4');
expect(midi2note(61, true)).toBe('C#4');
});
test('scaleStep', () => {
expect(scaleStep([60, 63, 67], 0)).toBe(60);
expect(scaleStep([60, 63, 67], 1)).toBe(63);
expect(scaleStep([60, 63, 67], 2)).toBe(67);
expect(scaleStep([60, 63, 67], 3)).toBe(72);
expect(scaleStep([60, 63, 67], 4)).toBe(75);
expect(scaleStep([60, 63, 67], -1)).toBe(55);
expect(scaleStep([60, 63, 67], -2)).toBe(51);
expect(scaleStep([60, 63, 67], -3)).toBe(48);
expect(scaleStep([60, 63, 67], -4)).toBe(43);
});
test('renderVoicing', () => {
const dictionary = {
m7: [
'3 7 10 14', // b3 5 b7 9
'10 14 15 19', // b7 9 b3 5
],
};
expect(renderVoicing({ chord: 'Em7', anchor: 'Bb4', dictionary, mode: 'below' })).toEqual([
'G3',
'B3',
'D4',
'Gb4',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'D5', dictionary, mode: 'below' })).toEqual([
'Eb4',
'G4',
'Bb4',
'D5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'G5', dictionary, mode: 'below' })).toEqual([
'Bb4',
'D5',
'Eb5',
'G5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below' })).toEqual([
'Bb4',
'D5',
'Eb5',
'G5',
]);
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 0 })).toEqual([70]); // Bb4
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 1 })).toEqual([74]); // D5
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', n: 4 })).toEqual([82]); // Bb5
expect(renderVoicing({ chord: 'Cm7', anchor: 'g5', dictionary, mode: 'below', offset: 1 })).toEqual([
'Eb5',
'G5',
'Bb5',
'D6',
]);
// expect(voiceBelow('G4', 'Cm7', voicingDictionary)).toEqual(['Bb3', 'D4', 'Eb4', 'G4']);
// TODO: test with offset
});
});
+6 -6
View File
@@ -127,18 +127,18 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
*
* The root note defaults to octave 3, if no octave number is given.
*
* @memberof Pattern
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* n("0 2 4 6 4 2").scale("C:major")
* "0 2 4 6 4 2".scale("C2:major").note()
* @example
* n("[0,7] 4 [2,7] 4")
* .scale("C:<major minor>/2")
* .s("piano")
* "0 2 4 6 4 2"
* .scale("C2:<major minor>")
* .note()
* @example
* n(rand.range(0,12).segment(8).round())
* .scale("C:ritusen")
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
* .s("folkharp")
*/
-190
View File
@@ -1,190 +0,0 @@
import { isNote, isNoteWithOctave, _mod, noteToMidi, tokenizeNote } from '@strudel.cycles/core';
import { Interval } from '@tonaljs/tonal';
// https://codesandbox.io/s/stateless-voicings-g2tmz0?file=/src/lib.js:0-2515
const flats = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
const pcs = ['c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'bb', 'b'];
const sharps = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const accs = { b: -1, '#': 1 };
export const pc2chroma = (pc) => {
const [letter, ...rest] = pc.split('');
return pcs.indexOf(letter.toLowerCase()) + rest.reduce((sum, sign) => sum + accs[sign], 0);
};
export const rotateChroma = (chroma, steps) => (chroma + (steps % 12) + 12) % 12;
export const chroma2pc = (chroma, sharp = false) => {
return (sharp ? sharps : flats)[chroma];
};
export function tokenizeChord(chord) {
const match = (chord || '').match(/^([A-G][b#]*)([^/]*)[/]?([A-G][b#]*)?$/);
if (!match) {
// console.warn('could not tokenize chord', chord);
return [];
}
return match.slice(1);
}
export const note2pc = (note) => note.match(/^[A-G][#b]?/i)[0];
export const note2oct = (note) => tokenizeNote(note)[2];
export const note2chroma = (note) => {
return pc2chroma(note2pc(note));
};
// TODO: test
export const midi2chroma = (midi) => midi % 12;
// TODO: test and use in voicing function
export const pitch2chroma = (x, defaultOctave) => {
if (isNoteWithOctave(x)) {
return note2chroma(x);
}
if (isNote(x)) {
//pc
return pc2chroma(x, defaultOctave);
}
if (typeof x === 'number') {
// expect midi
return midi2chroma(x);
}
};
export const step2semitones = (x) => {
let num = Number(x);
if (!isNaN(num)) {
return num;
}
return Interval.semitones(x);
};
export const x2midi = (x) => {
if (typeof x === 'number') {
return x;
}
if (typeof x === 'string') {
return noteToMidi(x);
}
};
// duplicate: util.mjs (does not support sharp flag)
export const midi2note = (midi, sharp = false) => {
const oct = Math.floor(midi / 12) - 1;
const pc = (sharp ? sharps : flats)[midi % 12];
return pc + oct;
};
export function scaleStep(notes, offset, octaves = 1) {
notes = notes.map((note) => (typeof note === 'string' ? noteToMidi(note) : note));
const octOffset = Math.floor(offset / notes.length) * octaves * 12;
offset = _mod(offset, notes.length);
return notes[offset] + octOffset;
}
// different ways to resolve the note to compare the anchor to (see renderVoicing)
let modeTarget = {
below: (v) => v.slice(-1)[0],
duck: (v) => v.slice(-1)[0],
above: (v) => v[0],
};
export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below', anchor = 'c5', octaves = 1 }) {
const [root, symbol] = tokenizeChord(chord);
const rootChroma = pc2chroma(root);
anchor = anchor?.note || anchor;
const anchorChroma = pitch2chroma(anchor);
const voicings = dictionary[symbol].map((voicing) =>
(typeof voicing === 'string' ? voicing.split(' ') : voicing).map(step2semitones),
);
let minDistance, bestIndex;
// calculate distances up from voicing top notes
let chromaDiffs = voicings.map((v, i) => {
const targetStep = modeTarget[mode](v);
const diff = _mod(anchorChroma - targetStep - rootChroma, 12);
if (minDistance === undefined || diff < minDistance) {
minDistance = diff;
bestIndex = i;
}
return diff;
});
const octDiff = Math.ceil(offset / voicings.length) * 12;
const indexWithOffset = _mod(bestIndex + offset, voicings.length);
const voicing = voicings[indexWithOffset];
const targetStep = modeTarget[mode](voicing);
const anchorMidi = noteToMidi(anchor, 4) - chromaDiffs[indexWithOffset] + octDiff;
const voicingMidi = voicing.map((v) => anchorMidi - targetStep + v);
let notes = voicingMidi.map((n) => midi2note(n));
if (mode === 'duck') {
notes = notes.filter((_, i) => voicingMidi[i] !== noteToMidi(anchor));
}
if (n !== undefined) {
return [scaleStep(notes, n, octaves)];
}
return notes;
}
// https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs
const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7];
const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B'];
const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
export const accidentalOffset = (accidentals) => {
return accidentals.split('#').length - accidentals.split('b').length;
};
const accidentalString = (offset) => {
if (offset < 0) {
return 'b'.repeat(-offset);
}
if (offset > 0) {
return '#'.repeat(offset);
}
return '';
};
export const Step = {
tokenize(step) {
const matches = step.match(/^([#b]*)([1-9][0-9]*)$/);
if (!matches) {
throw new Error(`Step.tokenize: not a valid step: ${step}`);
}
const [accidentals, stepNumber] = matches.slice(1);
return [accidentals, parseInt(stepNumber)];
},
accidentals(step) {
return accidentalOffset(Step.tokenize(step)[0]);
},
};
export const Note = {
// TODO: support octave numbers
tokenize(note) {
return [note[0], note.slice(1)];
},
accidentals(note) {
return accidentalOffset(this.tokenize(note)[1]);
},
};
// TODO: support octave numbers
export function transpose(note, step) {
// example: E, 3
const stepNumber = Step.tokenize(step)[1]; // 3
const noteLetter = Note.tokenize(note)[0]; // E
const noteIndex = noteLetters.indexOf(noteLetter); // 2 "E is C+2"
const targetNote = noteLetters[(noteIndex + stepNumber - 1) % 8]; // G "G is a third above E"
const rootIndex = notes.indexOf(noteLetter); // 4 "E is 4 semitones above C"
const targetIndex = notes.indexOf(targetNote); // 7 "G is 7 semitones above C"
const indexOffset = targetIndex - rootIndex; // 3 (E to G is normally a 3 semitones)
const stepIndex = steps.indexOf(stepNumber); // 4 ("3" is normally 4 semitones)
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
return [targetNote, offsetAccidentals].join('');
}
//Note("Bb3").transpose("c3")
+5 -105
View File
@@ -5,9 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { stack, register } from '@strudel.cycles/core';
import { renderVoicing } from './tonleiter.mjs';
import _voicings from 'chord-voicings';
import { complex, simple } from './ireal.mjs';
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
const lefthand = {
@@ -51,33 +49,10 @@ const triads = {
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
};
const defaultDictionary = {
// triads
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
// sevenths chords
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
69: ['3M 5P 6A 9M'],
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
'7#11': ['7m 9M 11A 13A'],
'7#9': ['3M 7m 9A'],
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
};
export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'], mode: 'below', anchor: 'a4' },
triads: { dictionary: triads, mode: 'below', anchor: 'a4' },
guidetones: { dictionary: guidetones, mode: 'above', anchor: 'a4' },
default: { dictionary: defaultDictionary, mode: 'below', anchor: 'a4' },
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
triads: { dictionary: triads },
guidetones: { dictionary: guidetones },
};
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
@@ -106,11 +81,6 @@ export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => {
Object.assign(voicingRegistry, { [name]: { dictionary, range } });
};
// new call signature
export const registerVoicings = (name, dictionary, options = {}) => {
Object.assign(voicingRegistry, { [name]: { dictionary, ...options } });
};
const getVoicing = (chord, dictionaryName, lastVoicing) => {
const { dictionary, range } = voicingRegistry[dictionaryName];
return dictionaryVoicing({
@@ -123,7 +93,6 @@ const getVoicing = (chord, dictionaryName, lastVoicing) => {
};
/**
* DEPRECATED: still works, but it is recommended you use .voicing instead (without s).
* Turns chord symbols into voicings, using the smoothest voice leading possible.
* Uses [chord-voicings package](https://github.com/felixroos/chord-voicings#chord-voicings).
*
@@ -160,76 +129,7 @@ export const voicings = register('voicings', function (dictionary, pat) {
*/
export const rootNotes = register('rootNotes', function (octave, pat) {
return pat.fmap((value) => {
const chord = value.chord || value;
const root = chord.match(/^([a-gA-G][b#]?).*$/)[1];
const note = root + octave;
return value.chord ? { note } : note;
const root = value.match(/^([a-gA-G][b#]?).*$/)[1];
return root + octave;
});
});
/**
* Turns chord symbols into voicings. You can use the following control params:
*
* - `chord`: Note, followed by chord symbol, e.g. C Am G7 Bb^7
* - `dict`: voicing dictionary to use, falls back to default dictionary
* - `anchor`: the note that is used to align the chord
* - `mode`: how the voicing is aligned to the anchor
* - `below`: top note <= anchor
* - `duck`: top note <= anchor, anchor excluded
* - `above`: bottom note >= anchor
* - `offset`: whole number that shifts the voicing up or down to the next voicing
* - `n`: if set, the voicing is played like a scale. Overshooting numbers will be octaved
*
* All of the above controls are optional, except `chord`.
* If you pass a pattern of strings to voicing, they will be interpreted as chords.
*
* @name voicing
* @param {string} dictionary which voicing dictionary to use.
* @returns Pattern
* @example
* voicing("<C Am F G>")
* @example
* n("0 1 2 3 4 5 6 7").chord("<C Am F G>").voicing()
*/
export const voicing = register('voicing', function (pat) {
return pat
.fmap((value) => {
// destructure voicing controls out
value = typeof value === 'string' ? { chord: value } : value;
let { dictionary = 'default', chord, anchor, offset, mode, n, octaves, ...rest } = value;
dictionary =
typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' };
let notes = renderVoicing({ ...dictionary, chord, anchor, offset, mode, n, octaves });
return stack(...notes)
.note()
.set(rest); // rest does not include voicing controls anymore!
})
.outerJoin();
});
export function voicingAlias(symbol, alias, setOrSets) {
setOrSets = !Array.isArray(setOrSets) ? [setOrSets] : setOrSets;
setOrSets.forEach((set) => {
set[alias] = set[symbol];
});
}
// no symbol = major chord
voicingAlias('^', '', [simple, complex]);
Object.keys(simple).forEach((symbol) => {
// add aliases for "-" === "m"
if (symbol.includes('-')) {
let alias = symbol.replace('-', 'm');
voicingAlias(symbol, alias, [complex, simple]);
}
// add aliases for "^" === "M"
if (symbol.includes('^')) {
let alias = symbol.replace('^', 'M');
voicingAlias(symbol, alias, [complex, simple]);
}
});
registerVoicings('ireal', simple);
registerVoicings('ireal-ext', complex);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/transpiler",
"version": "0.9.0",
"version": "0.8.2",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"publishConfig": {
@@ -38,6 +38,6 @@
},
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vitest": "^0.28.0"
}
}
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@strudel/web REPL Example</title>
</head>
<body>
<div id="app"></div>
<canvas style="background: #000" width="320" height="320" id="canvas"></canvas><br />
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from '@strudel/web';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let drawer;
let init = initStrudel({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
onToggle: async (started) => {
const { scheduler } = await init;
if (started) {
drawer.start(scheduler);
} else {
drawer.stop();
}
},
}).then(({ scheduler }) => {
drawer = new Drawer(
(haps, time, { drawTime }) => {
scheduler.pattern.context?.onPaint?.(ctx, time, haps, drawTime);
},
[-2, 2],
);
return { scheduler };
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev).spiral({size:20})`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).spiral({ size: 20, steady: 0.9 }).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).punchcard().play());
click('stop', () => hush());
</script>
</body>
</html>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "0.9.0",
"version": "0.8.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "web.mjs",
"publishConfig": {
+3 -1
View File
@@ -29,7 +29,7 @@ export async function defaultPrebake() {
let initDone;
let scheduler;
export function initStrudel(options = {}) {
export async function initStrudel(options = {}) {
initAudioOnFirstClick();
miniAllStrings();
const { prebake, ...schedulerOptions } = options;
@@ -39,6 +39,8 @@ export function initStrudel(options = {}) {
await prebake?.();
})();
scheduler = webaudioScheduler(schedulerOptions);
await initDone;
return { scheduler };
}
window.initStrudel = initStrudel;
-1
View File
@@ -1,7 +1,6 @@
# @strudel.cycles/webaudio
This package contains helpers to make music with strudel and the Web Audio API.
It is a thin binding to [superdough](https://www.npmjs.com/package/superdough).
## Install
@@ -1,5 +1,4 @@
import { getAudioContext } from './superdough.mjs';
import { clamp } from './util.mjs';
import { getAudioContext } from './webaudio.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
@@ -7,6 +6,17 @@ export function gainNode(value) {
return node;
}
export const getOscillator = ({ s, freq, t }) => {
// make oscillator
const o = getAudioContext().createOscillator();
o.type = s || 'triangle';
o.frequency.value = Number(freq);
o.start(t);
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
};
// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future
export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => {
const gainNode = getAudioContext().createGain();
@@ -29,22 +39,6 @@ export const getEnvelope = (attack, decay, sustain, release, velocity, begin) =>
};
};
export const getExpEnvelope = (attack, decay, sustain, release, velocity, begin) => {
sustain = Math.max(0.001, sustain);
velocity = Math.max(0.001, velocity);
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0.0001, begin);
gainNode.gain.exponentialRampToValueAtTime(velocity, begin + attack);
gainNode.gain.exponentialRampToValueAtTime(sustain * velocity, begin + attack + decay);
return {
node: gainNode,
stop: (t) => {
// similar to getEnvelope, this will glitch if sustain level has not been reached
gainNode.gain.exponentialRampToValueAtTime(0.0001, t + release);
},
};
};
export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
@@ -67,48 +61,10 @@ export const getADSR = (attack, decay, sustain, release, velocity, begin, end) =
return gainNode;
};
export const getParamADSR = (param, attack, decay, sustain, release, min, max, begin, end) => {
const range = max - min;
const peak = min + range;
const sustainLevel = min + sustain * range;
param.setValueAtTime(min, begin);
param.linearRampToValueAtTime(peak, begin + attack);
param.linearRampToValueAtTime(sustainLevel, begin + attack + decay);
param.setValueAtTime(sustainLevel, end);
param.linearRampToValueAtTime(min, end + Math.max(release, 0.1));
};
export function createFilter(
context,
type,
frequency,
Q,
attack,
decay,
sustain,
release,
fenv,
start,
end,
fanchor = 0.5,
) {
const filter = context.createBiquadFilter();
export const getFilter = (type, frequency, Q) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.Q.value = Q;
filter.frequency.value = frequency;
// Apply ADSR to filter frequency
if (!isNaN(fenv) && fenv !== 0) {
const offset = fenv * fanchor;
const min = clamp(2 ** -offset * frequency, 0, 20000);
const max = clamp(2 ** (fenv - offset) * frequency, 0, 20000);
// console.log('min', min, 'max', max);
getParamADSR(filter.frequency, attack, decay, sustain, release, min, max, start, end);
return filter;
}
filter.Q.value = Q;
return filter;
}
};
+3 -2
View File
@@ -5,5 +5,6 @@ This program is free software: you can redistribute it and/or modify it under th
*/
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from 'superdough';
export * from './sampler.mjs';
export * from './helpers.mjs';
export * from './synth.mjs';
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.9.0",
"version": "0.8.2",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,7 +35,7 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"superdough": "workspace:*"
"nanostores": "^0.8.1"
},
"devDependencies": {
"vite": "^4.3.3"
@@ -1,7 +1,6 @@
import { noteToMidi, valueToMidi } from './util.mjs';
import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, registerSound } from './index.mjs';
import { getEnvelope } from './helpers.mjs';
import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
@@ -196,7 +195,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
let {
const {
s,
freq,
unit,
@@ -207,9 +206,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
n = 0,
note,
speed = 1, // sample playback speed
loopBegin = 0,
begin = 0,
loopEnd = 1,
end = 1,
} = value;
// load sample
@@ -217,7 +214,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
// 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
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
@@ -245,12 +241,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
bufferSource.start(time, offset);
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
/*if (loop) {
// TODO: idea for loopBegin / loopEnd
// if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized
// if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number
// this will simplify perfectly looping things, while still keeping the normalized option
// the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case)
bufferSource.loop = true;
bufferSource.loopStart = offset;
bufferSource.loopEnd = offset + duration;
duration = loop * duration;
}*/
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
bufferSource.connect(envelope);
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
@@ -261,10 +264,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
out.disconnect();
onended();
};
const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => {
const stop = (endTime, playWholeBuffer = clip === undefined) => {
let releaseTime = endTime;
if (playWholeBuffer) {
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
releaseTime = t + (end - begin) * bufferDuration;
}
bufferSource.stop(releaseTime + release);
-88
View File
@@ -1,88 +0,0 @@
import { Pattern, getDrawContext, clamp } from '@strudel.cycles/core';
import { analyser, getAnalyzerData } from 'superdough';
export function drawTimeScope(
analyser,
{ align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, next = 1, trigger = 0 } = {},
) {
const ctx = getDrawContext();
const dataArray = getAnalyzerData('time');
ctx.lineWidth = thickness;
ctx.strokeStyle = color;
ctx.beginPath();
let canvas = ctx.canvas;
const bufferSize = analyser.frequencyBinCount;
let triggerIndex = align
? Array.from(dataArray).findIndex((v, i, arr) => i && arr[i - 1] > -trigger && v <= -trigger)
: 0;
triggerIndex = Math.max(triggerIndex, 0); // fallback to 0 when no trigger is found
const sliceWidth = (canvas.width * 1.0) / bufferSize;
let x = 0;
for (let i = triggerIndex; i < bufferSize; i++) {
const v = dataArray[i] + 1;
const y = (scale * (v - 1) + pos) * canvas.height;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.stroke();
}
export function drawFrequencyScope(
analyser,
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {},
) {
const dataArray = getAnalyzerData('frequency');
const ctx = getDrawContext();
const canvas = ctx.canvas;
ctx.fillStyle = color;
const bufferSize = analyser.frequencyBinCount;
const sliceWidth = (canvas.width * 1.0) / bufferSize;
let x = 0;
for (let i = 0; i < bufferSize; i++) {
const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1);
const v = normalized * scale;
const h = v * canvas.height;
const y = (pos - v * lean) * canvas.height;
ctx.fillRect(x, y, Math.max(sliceWidth, 1), h);
x += sliceWidth;
}
}
function clearScreen(smear = 0, smearRGB = `0,0,0`) {
const ctx = getDrawContext();
if (!smear) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
} else {
ctx.fillStyle = `rgba(${smearRGB},${1 - smear})`;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
}
Pattern.prototype.fscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawFrequencyScope(analyser, config);
});
};
Pattern.prototype.tscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawTimeScope(analyser, config);
});
};
Pattern.prototype.scope = Pattern.prototype.tscope;
+44
View File
@@ -0,0 +1,44 @@
import { midiToFreq, noteToMidi } from '@strudel.cycles/core';
import { registerSound } from './webaudio.mjs';
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
export function registerSynthSounds() {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
n = noteToMidi(n); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof n === 'number') {
freq = midiToFreq(n); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({ t, s: wave, freq });
const g = gainNode(0.3);
// envelope
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
return {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
stop(releaseTime + release);
},
};
},
{ type: 'synth', prebake: true },
);
});
}
+234 -8
View File
@@ -5,21 +5,247 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { superdough, getAudioContext, setLogger } from 'superdough';
import './feedbackdelay.mjs';
import './reverb.mjs';
const { Pattern, logger } = strudel;
import './vowel.mjs';
import workletsUrl from './worklets.mjs?url';
import { getFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
setLogger(logger);
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
export function getSound(s) {
return soundMap.get()[s];
}
export const resetLoadedSounds = () => soundMap.set({});
const hap2value = (hap) => {
hap.ensureObjectValue();
return { ...hap.value, velocity: hap.context.velocity };
let audioContext;
export const getAudioContext = () => {
if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext;
};
// TODO: bind logger
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
export const webaudioOutput = (hap, deadline, hapDuration) => superdough(hap2value(hap), deadline, hapDuration);
let destination;
const getDestination = () => {
const ctx = getAudioContext();
if (!destination) {
destination = ctx.createGain();
destination.connect(ctx.destination);
}
return destination;
};
export const panic = () => {
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
destination = null;
};
let workletsLoading;
function loadWorklets() {
if (workletsLoading) {
return workletsLoading;
}
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
return workletsLoading;
}
function getWorklet(ac, processor, params) {
const node = new AudioWorkletNode(ac, processor);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
});
return node;
}
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio() {
if (typeof window !== 'undefined') {
try {
await getAudioContext().resume();
await loadWorklets();
} catch (err) {
console.warn('could not load AudioWorklet effects coarse, crush and shape', err);
}
}
}
export async function initAudioOnFirstClick() {
return new Promise((resolve) => {
document.addEventListener('click', async function listener() {
await initAudio();
resolve();
document.removeEventListener('click', listener);
});
});
}
let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = strudel.clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
dly.connect(getDestination());
delays[orbit] = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
return delays[orbit];
}
let reverbs = {};
function getReverb(orbit, duration = 2) {
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration);
reverb.connect(getDestination());
reverbs[orbit] = reverb;
}
if (reverbs[orbit].duration !== duration) {
reverbs[orbit] = reverbs[orbit].setDuration(duration);
reverbs[orbit].duration = duration;
}
return reverbs[orbit];
}
function effectSend(input, effect, wet) {
const send = gainNode(wet);
input.connect(send);
send.connect(effect);
return send;
}
// export const webaudioOutput = async (t, hap, ct, cps) => {
export const webaudioOutput = async (hap, deadline, hapDuration, cps) => {
const ac = getAudioContext();
hap.ensureObjectValue();
// calculate absolute time
let t = ac.currentTime + deadline;
// destructure
let {
s = 'triangle',
bank,
source,
gain = 0.8,
// low pass
cutoff,
resonance = 1,
// high pass
hcutoff,
hresonance = 1,
// band pass
bandf,
bandq = 1,
//
coarse,
crush,
shape,
pan,
vowel,
delay = 0,
delayfeedback = 0.5,
delaytime = 0.25,
orbit = 1,
room,
size = 2,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) {
s = `${bank}_${s}`;
}
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, hap.value, hapDuration);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, hap.value, onended);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
}
} else {
throw new Error(`sound ${s} not found! Is it loaded?`);
}
if (!sourceNode) {
// if onTrigger does not return anything, we will just silently skip
// this can be used for things like speed(0) in the sampler
return;
}
if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return;
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
// gain stage
chain.push(gainNode(gain));
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// last gain
const post = gainNode(1);
chain.push(post);
post.connect(getDestination());
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delyNode, delay);
}
// reverb
let reverbSend;
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
reverbSend = effectSend(post, reverbNode, room);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend]);
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps);
Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.9.0",
"version": "0.8.0",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"publishConfig": {
@@ -34,6 +34,6 @@
},
"devDependencies": {
"vite": "^4.3.3",
"vitest": "^0.33.0"
"vitest": "^0.28.0"
}
}
+166 -478
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -6,4 +6,3 @@ packages:
- "packages/core/examples/vite-vanilla-repl-cm6"
- "packages/react/examples/nano-repl"
- "packages/web/examples/repl-example"
- "packages/superdough/example"
+7 -264
View File
@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
@@ -50,28 +41,6 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "alsa"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47"
dependencies = [
"alsa-sys",
"bitflags",
"libc",
"nix",
]
[[package]]
name = "alsa-sys"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -97,13 +66,10 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
name = "app"
version = "0.1.0"
dependencies = [
"midir",
"rosc",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tokio",
]
[[package]]
@@ -136,21 +102,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
@@ -427,26 +378,6 @@ dependencies = [
"libc",
]
[[package]]
name = "coremidi"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7847ca018a67204508b77cb9e6de670125075f7464fff5f673023378fa34f5"
dependencies = [
"core-foundation",
"core-foundation-sys",
"coremidi-sys",
]
[[package]]
name = "coremidi-sys"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79a6deed0c97b2d40abbab77e4c97f81d71e162600423382c277dd640019116c"
dependencies = [
"core-foundation-sys",
]
[[package]]
name = "cpufeatures"
version = "0.2.8"
@@ -979,12 +910,6 @@ dependencies = [
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
name = "gimli"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "gio"
version = "0.15.12"
@@ -1542,28 +1467,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "midir"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a456444d83e7ead06ae6a5c0a215ed70282947ff3897fb45fcb052b757284731"
dependencies = [
"alsa",
"bitflags",
"coremidi",
"js-sys",
"libc",
"wasm-bindgen",
"web-sys",
"windows 0.43.0",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@@ -1574,17 +1477,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "mio"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
name = "ndk"
version = "0.6.0"
@@ -1619,33 +1511,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -1745,15 +1616,6 @@ dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.18.0"
@@ -1920,9 +1782,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.13"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "pin-utils"
@@ -2190,22 +2052,6 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]]
name = "rosc"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e63d9e6b0d090be1485cf159b1e04c3973d2d3e1614963544ea2ff47a4a981"
dependencies = [
"byteorder",
"nom",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc_version"
version = "0.4.0"
@@ -2428,15 +2274,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.5"
@@ -2464,16 +2301,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "socket2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "soup2"
version = "0.2.1"
@@ -2958,34 +2785,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.32.0"
version = "1.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
dependencies = [
"backtrace",
"autocfg",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]]
name = "toml"
version = "0.5.11"
@@ -3280,16 +3090,6 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "web-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webkit2gtk"
version = "0.18.2"
@@ -3420,21 +3220,6 @@ dependencies = [
"windows_x86_64_msvc 0.39.0",
]
[[package]]
name = "windows"
version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows"
version = "0.48.0"
@@ -3485,12 +3270,12 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.48.0",
]
@@ -3500,12 +3285,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
@@ -3518,12 +3297,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
@@ -3536,12 +3309,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
@@ -3554,12 +3321,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
@@ -3572,24 +3333,12 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
@@ -3602,12 +3351,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
-3
View File
@@ -18,9 +18,6 @@ tauri-build = { version = "1.4.0", features = [] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = ["fs-all"] }
midir = "0.9.1"
tokio = { version = "1.29.0", features = ["full"] }
rosc = "0.10.1"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
-20
View File
@@ -1,20 +0,0 @@
use std::sync::Arc;
use tauri::Window;
#[derive(Clone, serde::Serialize)]
pub struct LoggerPayload {
pub message: String,
pub message_type: String,
}
#[derive(Clone)]
pub struct Logger {
pub window: Arc<Window>,
}
impl Logger {
pub fn log(&self, message: String, message_type: String) {
println!("{}", message);
let _ = self.window.emit("log-event", LoggerPayload { message, message_type });
}
}
+1 -40
View File
@@ -1,47 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod midibridge;
mod oscbridge;
mod loggerbridge;
use std::sync::Arc;
use loggerbridge::Logger;
use tauri::Manager;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
// the payload type must implement `Serialize` and `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
message: String,
message_type: String,
}
fn main() {
let (async_input_transmitter_midi, async_input_receiver_midi) = mpsc::channel(1);
let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1);
let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1);
let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1);
tauri::Builder
::default()
.manage(midibridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_midi),
})
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
.setup(|app| {
let window = Arc::new(app.get_window("main").unwrap());
let logger = Logger { window };
midibridge::init(
logger.clone(),
async_input_receiver_midi,
async_output_receiver_midi,
async_output_transmitter_midi
);
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
Ok(())
})
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
-158
View File
@@ -1,158 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use midir::MidiOutput;
use tokio::sync::{ mpsc, Mutex };
use tokio::time::Instant;
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
pub struct MidiMessage {
pub message: Vec<u8>,
pub instant: Instant,
pub offset: u64,
pub requestedport: String,
}
pub struct AsyncInputTransmit {
pub inner: Mutex<mpsc::Sender<Vec<MidiMessage>>>,
}
pub fn init(
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<MidiMessage>>,
mut async_output_receiver: mpsc::Receiver<Vec<MidiMessage>>,
async_output_transmitter: mpsc::Sender<Vec<MidiMessage>>
) {
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let message_queue: Arc<Mutex<Vec<MidiMessage>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Open Midi Ports
............................................................*/
let midiout = MidiOutput::new("strudel").unwrap();
let out_ports = midiout.ports();
let mut port_names = Vec::new();
//TODO: Send these print messages to the UI logger instead of the rust console so the user can see them
if out_ports.len() == 0 {
logger.log(
" No MIDI devices found. Connect a device or enable IAC Driver to enable midi.".to_string(),
"".to_string()
);
// logger(window, " No MIDI devices found. Connect a device or enable IAC Driver.".to_string(), None);
return;
}
// give the frontend couple seconds to load on start, or the log messages will get lost
sleep(Duration::from_secs(3));
logger.log(format!("Found {} midi devices!", out_ports.len()), "".to_string());
// the user could reference any port at anytime during runtime,
// so let's go ahead and open them all (same behavior as web app)
let mut output_connections = HashMap::new();
for i in 0..=out_ports.len().saturating_sub(1) {
let midiout = MidiOutput::new("strudel").unwrap();
let ports = midiout.ports();
let port = ports.get(i).unwrap();
let port_name = midiout.port_name(port).unwrap();
logger.log(port_name.clone(), "".to_string());
let out_con = midiout.connect(port, &port_name).unwrap();
port_names.insert(i, port_name.clone());
output_connections.insert(port_name, out_con);
}
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
//iterate over each message, play and remove messages when they are ready
message_queue.retain(|message| {
if message.instant.elapsed().as_millis() < message.offset.into() {
return true;
}
let mut out_con = output_connections.get_mut(&message.requestedport);
// WebMidi supports getting a connection by part of its name
// ex: 'bus 1' instead of 'IAC Driver bus 1' so let's emulate that behavior
if out_con.is_none() {
let key = port_names.iter().find(|port_name| {
return port_name.contains(&message.requestedport);
});
if key.is_some() {
out_con = output_connections.get_mut(key.unwrap());
}
}
if out_con.is_some() {
// process the message
if let Err(err) = (&mut out_con.unwrap()).send(&message.message) {
logger.log(format!("Midi message send error: {}", err), "error".to_string());
}
} else {
logger.log(format!("failed to find midi device: {}", message.requestedport), "error".to_string());
}
return false;
});
sleep(Duration::from_millis(1));
}
});
}
pub async fn async_process_model(
mut input_reciever: mpsc::Receiver<Vec<MidiMessage>>,
output_transmitter: mpsc::Sender<Vec<MidiMessage>>
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct MessageFromJS {
message: Vec<u8>,
offset: u64,
requestedport: String,
}
// Called from JS
#[tauri::command]
pub async fn sendmidi(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<MidiMessage> = Vec::new();
for m in messagesfromjs {
let message_to_process = MidiMessage {
instant: Instant::now(),
message: m.message,
offset: m.offset,
requestedport: m.requestedport,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}
-157
View File
@@ -1,157 +0,0 @@
use rosc::{ encoder, OscTime };
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
use std::net::UdpSocket;
use std::time::Duration;
use std::sync::Arc;
use tokio::sync::{ mpsc, Mutex };
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
pub struct OscMsg {
pub msg_buf: Vec<u8>,
pub timestamp: u64,
}
pub struct AsyncInputTransmit {
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
}
const UNIX_OFFSET: u64 = 2_208_988_800; // 70 years in seconds
const TWO_POW_32: f64 = (u32::MAX as f64) + 1.0; // Number of bits in a `u32`
const NANOS_PER_SECOND: f64 = 1.0e9;
const SECONDS_PER_NANO: f64 = 1.0 / NANOS_PER_SECOND;
pub fn init(
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>
) {
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Open OSC Ports
............................................................*/
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
let to_addr = String::from("127.0.0.1:57120");
sock.set_nonblocking(true).unwrap();
sock.connect(to_addr).expect("could not connect to OSC address");
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
message_queue.retain(|message| {
let result = sock.send(&message.msg_buf);
if result.is_err() {
logger.log(
format!("OSC Message failed to send, the server might no longer be available"),
"error".to_string()
);
}
return false;
});
sleep(Duration::from_millis(1));
}
});
}
pub async fn async_process_model(
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
output_transmitter: mpsc::Sender<Vec<OscMsg>>
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct Param {
name: String,
value: String,
valueisnumber: bool,
}
#[derive(Deserialize)]
pub struct MessageFromJS {
params: Vec<Param>,
timestamp: u64,
target: String,
}
// Called from JS
#[tauri::command]
pub async fn sendosc(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<OscMsg> = Vec::new();
for m in messagesfromjs {
let mut args = Vec::new();
for p in m.params {
args.push(OscType::String(p.name));
if p.valueisnumber {
args.push(OscType::Float(p.value.parse().unwrap()));
} else {
args.push(OscType::String(p.value));
}
}
let duration_since_epoch = Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
let seconds = u32
::try_from(duration_since_epoch.as_secs())
.map_err(|_| "bit conversion failed for osc message timetag")?;
let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
let timetag = OscTime::from((seconds, fractional));
let packet = OscPacket::Message(OscMessage {
addr: m.target,
args,
});
let bundle = OscBundle {
content: vec![packet],
timetag,
};
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
let message_to_process = OscMsg {
msg_buf,
timestamp: m.timestamp,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}
+110 -556
View File
@@ -1,4 +1,4 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Vitest Snapshot v1
exports[`runs examples > example "_euclidRot" example index 0 1`] = `
[
@@ -900,33 +900,6 @@ exports[`runs examples > example "begin" example index 0 1`] = `
]
`;
exports[`runs examples > example "bpattack" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
]
`;
exports[`runs examples > example "bpdecay" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0.2 bpenv:4 ]",
]
`;
exports[`runs examples > example "bpenv" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
]
`;
exports[`runs examples > example "bpf" example index 0 1`] = `
[
"[ 0/1 → 1/3 | s:hh bandf:1000 ]",
@@ -965,24 +938,6 @@ exports[`runs examples > example "bpq" example index 0 1`] = `
]
`;
exports[`runs examples > example "bprelease" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth clip:0.5 bandf:500 bpenv:4 bprelease:0.5 release:0.5 ]",
]
`;
exports[`runs examples > example "bpsustain" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth bandf:500 bpdecay:0.5 bpsustain:0 bpenv:4 ]",
]
`;
exports[`runs examples > example "cat" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:hh ]",
@@ -1828,15 +1783,6 @@ exports[`runs examples > example "firstOf" example index 0 1`] = `
]
`;
exports[`runs examples > example "fit" example index 0 1`] = `
[
"[ (0/1 → 1/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ 0/1 ⇜ (1/1 → 2/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ 0/1 ⇜ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
"[ 0/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.25 unit:c ]",
]
`;
exports[`runs examples > example "floor" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:42 ]",
@@ -1858,132 +1804,6 @@ exports[`runs examples > example "floor" example index 0 1`] = `
]
`;
exports[`runs examples > example "fm" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:0 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:0 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:0 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:0 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:1 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:1 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:1 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:1 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:2 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:2 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:2 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:2 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:8 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:8 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:8 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:8 analyze:1 ]",
]
`;
exports[`runs examples > example "fmattack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmattack:0 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmattack:0 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmattack:0 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmattack:0 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmattack:0.05 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmattack:0.05 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmattack:0.05 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmattack:0.05 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmattack:0.1 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmattack:0.1 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmattack:0.1 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmattack:0.1 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmattack:0.2 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmattack:0.2 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmattack:0.2 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmattack:0.2 analyze:1 ]",
]
`;
exports[`runs examples > example "fmdecay" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
]
`;
exports[`runs examples > example "fmenv" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]",
]
`;
exports[`runs examples > example "fmh" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmh:1 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmh:1 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmh:1 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmh:1 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmh:2 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmh:2 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmh:2 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmh:2 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmh:1.5 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmh:1.5 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmh:1.5 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmh:1.5 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmh:1.61 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmh:1.61 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmh:1.61 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmh:1.61 analyze:1 ]",
]
`;
exports[`runs examples > example "fmsustain" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
"[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
"[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
"[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
"[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
"[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
"[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
]
`;
exports[`runs examples > example "focus" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:sd ]",
@@ -2067,15 +1887,6 @@ exports[`runs examples > example "freq" example index 1 1`] = `
]
`;
exports[`runs examples > example "ftype" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 bpenv:4 ftype:12db ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 bpenv:4 ftype:24db ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 bpenv:4 ftype:12db ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 bpenv:4 ftype:24db ]",
]
`;
exports[`runs examples > example "gain" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh gain:0.4 ]",
@@ -2113,33 +1924,6 @@ exports[`runs examples > example "gain" example index 0 1`] = `
]
`;
exports[`runs examples > example "hpattack" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
]
`;
exports[`runs examples > example "hpdecay" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0.2 hpenv:4 ]",
]
`;
exports[`runs examples > example "hpenv" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
]
`;
exports[`runs examples > example "hpf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:hh hcutoff:4000 ]",
@@ -2227,24 +2011,6 @@ exports[`runs examples > example "hpq" example index 0 1`] = `
]
`;
exports[`runs examples > example "hprelease" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth clip:0.5 hcutoff:500 hpenv:4 hprelease:0.5 release:0.5 ]",
]
`;
exports[`runs examples > example "hpsustain" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth hcutoff:500 hpdecay:0.5 hpsustain:0 hpenv:4 ]",
]
`;
exports[`runs examples > example "hurry" example index 0 1`] = `
[
"[ 0/1 → 3/4 | s:bd speed:1 ]",
@@ -2649,10 +2415,10 @@ exports[`runs examples > example "linger" example index 0 1`] = `
exports[`runs examples > example "loop" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:casio loop:1 ]",
"[ 1/1 → 2/1 | s:casio loop:1 ]",
"[ 2/1 → 3/1 | s:casio loop:1 ]",
"[ 3/1 → 4/1 | s:casio loop:1 ]",
"[ 0/1 → 1/1 | s:bd loop:1 ]",
"[ 1/1 → 2/1 | s:bd loop:2 ]",
"[ 2/1 → 3/1 | s:bd loop:3 ]",
"[ 3/1 → 4/1 | s:bd loop:4 ]",
]
`;
@@ -2674,51 +2440,6 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = `
]
`;
exports[`runs examples > example "loopBegin" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:space loop:1 loopBegin:0 analyze:1 ]",
"[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 analyze:1 ]",
"[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 analyze:1 ]",
"[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 analyze:1 ]",
]
`;
exports[`runs examples > example "loopEnd" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 analyze:1 ]",
"[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 analyze:1 ]",
"[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 analyze:1 ]",
"[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 analyze:1 ]",
]
`;
exports[`runs examples > example "lpattack" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
]
`;
exports[`runs examples > example "lpdecay" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0.2 lpenv:4 ]",
]
`;
exports[`runs examples > example "lpenv" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]",
]
`;
exports[`runs examples > example "lpf" example index 0 1`] = `
[
"[ 0/1 → 1/3 | s:hh cutoff:4000 ]",
@@ -2810,24 +2531,6 @@ exports[`runs examples > example "lpq" example index 0 1`] = `
]
`;
exports[`runs examples > example "lprelease" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth clip:0.5 cutoff:500 lpenv:4 lprelease:0.5 release:0.5 ]",
]
`;
exports[`runs examples > example "lpsustain" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
"[ 1/1 → 2/1 | note:e2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
"[ 2/1 → 3/1 | note:f2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
"[ 3/1 → 4/1 | note:g2 s:sawtooth cutoff:500 lpdecay:0.5 lpsustain:0 lpenv:4 ]",
]
`;
exports[`runs examples > example "lrate" example index 0 1`] = `
[
"[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]",
@@ -3842,96 +3545,96 @@ exports[`runs examples > example "saw" example index 1 1`] = `
exports[`runs examples > example "scale" example index 0 1`] = `
[
"[ 0/1 → 1/6 | n:0 note:C3 ]",
"[ 1/6 → 1/3 | n:2 note:E3 ]",
"[ 1/3 → 1/2 | n:4 note:G3 ]",
"[ 1/2 → 2/3 | n:6 note:B3 ]",
"[ 2/3 → 5/6 | n:4 note:G3 ]",
"[ 5/6 → 1/1 | n:2 note:E3 ]",
"[ 1/1 → 7/6 | n:0 note:C3 ]",
"[ 7/6 → 4/3 | n:2 note:E3 ]",
"[ 4/3 → 3/2 | n:4 note:G3 ]",
"[ 3/2 → 5/3 | n:6 note:B3 ]",
"[ 5/3 → 11/6 | n:4 note:G3 ]",
"[ 11/6 → 2/1 | n:2 note:E3 ]",
"[ 2/1 → 13/6 | n:0 note:C3 ]",
"[ 13/6 → 7/3 | n:2 note:E3 ]",
"[ 7/3 → 5/2 | n:4 note:G3 ]",
"[ 5/2 → 8/3 | n:6 note:B3 ]",
"[ 8/3 → 17/6 | n:4 note:G3 ]",
"[ 17/6 → 3/1 | n:2 note:E3 ]",
"[ 3/1 → 19/6 | n:0 note:C3 ]",
"[ 19/6 → 10/3 | n:2 note:E3 ]",
"[ 10/3 → 7/2 | n:4 note:G3 ]",
"[ 7/2 → 11/3 | n:6 note:B3 ]",
"[ 11/3 → 23/6 | n:4 note:G3 ]",
"[ 23/6 → 4/1 | n:2 note:E3 ]",
"[ 0/1 → 1/6 | note:C2 ]",
"[ 1/6 → 1/3 | note:E2 ]",
"[ 1/3 → 1/2 | note:G2 ]",
"[ 1/2 → 2/3 | note:B2 ]",
"[ 2/3 → 5/6 | note:G2 ]",
"[ 5/6 → 1/1 | note:E2 ]",
"[ 1/1 → 7/6 | note:C2 ]",
"[ 7/6 → 4/3 | note:E2 ]",
"[ 4/3 → 3/2 | note:G2 ]",
"[ 3/2 → 5/3 | note:B2 ]",
"[ 5/3 → 11/6 | note:G2 ]",
"[ 11/6 → 2/1 | note:E2 ]",
"[ 2/1 → 13/6 | note:C2 ]",
"[ 13/6 → 7/3 | note:E2 ]",
"[ 7/3 → 5/2 | note:G2 ]",
"[ 5/2 → 8/3 | note:B2 ]",
"[ 8/3 → 17/6 | note:G2 ]",
"[ 17/6 → 3/1 | note:E2 ]",
"[ 3/1 → 19/6 | note:C2 ]",
"[ 19/6 → 10/3 | note:E2 ]",
"[ 10/3 → 7/2 | note:G2 ]",
"[ 7/2 → 11/3 | note:B2 ]",
"[ 11/3 → 23/6 | note:G2 ]",
"[ 23/6 → 4/1 | note:E2 ]",
]
`;
exports[`runs examples > example "scale" example index 1 1`] = `
[
"[ 0/1 → 1/4 | n:0 note:C3 s:piano ]",
"[ 0/1 → 1/4 | n:7 note:C4 s:piano ]",
"[ 1/4 → 1/2 | n:4 note:G3 s:piano ]",
"[ 1/2 → 3/4 | n:2 note:E3 s:piano ]",
"[ 1/2 → 3/4 | n:7 note:C4 s:piano ]",
"[ 3/4 → 1/1 | n:4 note:G3 s:piano ]",
"[ 1/1 → 5/4 | n:0 note:C3 s:piano ]",
"[ 1/1 → 5/4 | n:7 note:C4 s:piano ]",
"[ 5/4 → 3/2 | n:4 note:G3 s:piano ]",
"[ 3/2 → 7/4 | n:2 note:E3 s:piano ]",
"[ 3/2 → 7/4 | n:7 note:C4 s:piano ]",
"[ 7/4 → 2/1 | n:4 note:G3 s:piano ]",
"[ 2/1 → 9/4 | n:0 note:C3 s:piano ]",
"[ 2/1 → 9/4 | n:7 note:C4 s:piano ]",
"[ 9/4 → 5/2 | n:4 note:G3 s:piano ]",
"[ 5/2 → 11/4 | n:2 note:Eb3 s:piano ]",
"[ 5/2 → 11/4 | n:7 note:C4 s:piano ]",
"[ 11/4 → 3/1 | n:4 note:G3 s:piano ]",
"[ 3/1 → 13/4 | n:0 note:C3 s:piano ]",
"[ 3/1 → 13/4 | n:7 note:C4 s:piano ]",
"[ 13/4 → 7/2 | n:4 note:G3 s:piano ]",
"[ 7/2 → 15/4 | n:2 note:Eb3 s:piano ]",
"[ 7/2 → 15/4 | n:7 note:C4 s:piano ]",
"[ 15/4 → 4/1 | n:4 note:G3 s:piano ]",
"[ 0/1 → 1/6 | note:C2 ]",
"[ 1/6 → 1/3 | note:E2 ]",
"[ 1/3 → 1/2 | note:G2 ]",
"[ 1/2 → 2/3 | note:B2 ]",
"[ 2/3 → 5/6 | note:G2 ]",
"[ 5/6 → 1/1 | note:E2 ]",
"[ 1/1 → 7/6 | note:C2 ]",
"[ 7/6 → 4/3 | note:Eb2 ]",
"[ 4/3 → 3/2 | note:G2 ]",
"[ 3/2 → 5/3 | note:Bb2 ]",
"[ 5/3 → 11/6 | note:G2 ]",
"[ 11/6 → 2/1 | note:Eb2 ]",
"[ 2/1 → 13/6 | note:C2 ]",
"[ 13/6 → 7/3 | note:E2 ]",
"[ 7/3 → 5/2 | note:G2 ]",
"[ 5/2 → 8/3 | note:B2 ]",
"[ 8/3 → 17/6 | note:G2 ]",
"[ 17/6 → 3/1 | note:E2 ]",
"[ 3/1 → 19/6 | note:C2 ]",
"[ 19/6 → 10/3 | note:Eb2 ]",
"[ 10/3 → 7/2 | note:G2 ]",
"[ 7/2 → 11/3 | note:Bb2 ]",
"[ 11/3 → 23/6 | note:G2 ]",
"[ 23/6 → 4/1 | note:Eb2 ]",
]
`;
exports[`runs examples > example "scale" example index 2 1`] = `
[
"[ 0/1 → 1/8 | n:10 note:C5 s:folkharp ]",
"[ 1/8 → 1/4 | n:2 note:F3 s:folkharp ]",
"[ 1/4 → 3/8 | n:7 note:F4 s:folkharp ]",
"[ 3/8 → 1/2 | n:4 note:A3 s:folkharp ]",
"[ 1/2 → 5/8 | n:2 note:F3 s:folkharp ]",
"[ 5/8 → 3/4 | n:5 note:C4 s:folkharp ]",
"[ 3/4 → 7/8 | n:9 note:A4 s:folkharp ]",
"[ 7/8 → 1/1 | n:8 note:G4 s:folkharp ]",
"[ 1/1 → 9/8 | n:7 note:F4 s:folkharp ]",
"[ 9/8 → 5/4 | n:1 note:D3 s:folkharp ]",
"[ 5/4 → 11/8 | n:1 note:D3 s:folkharp ]",
"[ 11/8 → 3/2 | n:6 note:D4 s:folkharp ]",
"[ 3/2 → 13/8 | n:2 note:F3 s:folkharp ]",
"[ 13/8 → 7/4 | n:4 note:A3 s:folkharp ]",
"[ 7/4 → 15/8 | n:6 note:D4 s:folkharp ]",
"[ 15/8 → 2/1 | n:10 note:C5 s:folkharp ]",
"[ 2/1 → 17/8 | n:4 note:A3 s:folkharp ]",
"[ 17/8 → 9/4 | n:0 note:C3 s:folkharp ]",
"[ 9/4 → 19/8 | n:8 note:G4 s:folkharp ]",
"[ 19/8 → 5/2 | n:2 note:F3 s:folkharp ]",
"[ 5/2 → 21/8 | n:7 note:F4 s:folkharp ]",
"[ 21/8 → 11/4 | n:6 note:D4 s:folkharp ]",
"[ 11/4 → 23/8 | n:11 note:D5 s:folkharp ]",
"[ 23/8 → 3/1 | n:3 note:G3 s:folkharp ]",
"[ 3/1 → 25/8 | n:0 note:C3 s:folkharp ]",
"[ 25/8 → 13/4 | n:11 note:D5 s:folkharp ]",
"[ 13/4 → 27/8 | n:4 note:A3 s:folkharp ]",
"[ 27/8 → 7/2 | n:9 note:A4 s:folkharp ]",
"[ 7/2 → 29/8 | n:10 note:C5 s:folkharp ]",
"[ 29/8 → 15/4 | n:12 note:F5 s:folkharp ]",
"[ 15/4 → 31/8 | n:1 note:D3 s:folkharp ]",
"[ 31/8 → 4/1 | n:4 note:A3 s:folkharp ]",
"[ 0/1 → 1/8 | note:C3 s:folkharp ]",
"[ 1/8 → 1/4 | note:B2 s:folkharp ]",
"[ 1/4 → 3/8 | note:A2 s:folkharp ]",
"[ 3/8 → 1/2 | note:G2 s:folkharp ]",
"[ 1/2 → 5/8 | note:F2 s:folkharp ]",
"[ 5/8 → 3/4 | note:E2 s:folkharp ]",
"[ 3/4 → 7/8 | note:D2 s:folkharp ]",
"[ 7/8 → 1/1 | note:C2 s:folkharp ]",
"[ 1/1 → 9/8 | note:C3 s:folkharp ]",
"[ 9/8 → 5/4 | note:Bb2 s:folkharp ]",
"[ 5/4 → 11/8 | note:Ab2 s:folkharp ]",
"[ 11/8 → 3/2 | note:G2 s:folkharp ]",
"[ 3/2 → 13/8 | note:F2 s:folkharp ]",
"[ 13/8 → 7/4 | note:Eb2 s:folkharp ]",
"[ 7/4 → 15/8 | note:D2 s:folkharp ]",
"[ 15/8 → 2/1 | note:C2 s:folkharp ]",
"[ 2/1 → 17/8 | note:C3 s:folkharp ]",
"[ 17/8 → 9/4 | note:B2 s:folkharp ]",
"[ 9/4 → 19/8 | note:A2 s:folkharp ]",
"[ 19/8 → 5/2 | note:G2 s:folkharp ]",
"[ 5/2 → 21/8 | note:F2 s:folkharp ]",
"[ 21/8 → 11/4 | note:E2 s:folkharp ]",
"[ 11/4 → 23/8 | note:D2 s:folkharp ]",
"[ 23/8 → 3/1 | note:C2 s:folkharp ]",
"[ 3/1 → 25/8 | note:C3 s:folkharp ]",
"[ 25/8 → 13/4 | note:Bb2 s:folkharp ]",
"[ 13/4 → 27/8 | note:Ab2 s:folkharp ]",
"[ 27/8 → 7/2 | note:G2 s:folkharp ]",
"[ 7/2 → 29/8 | note:F2 s:folkharp ]",
"[ 29/8 → 15/4 | note:Eb2 s:folkharp ]",
"[ 15/4 → 31/8 | note:D2 s:folkharp ]",
"[ 31/8 → 4/1 | note:C2 s:folkharp ]",
]
`;
@@ -4171,60 +3874,6 @@ exports[`runs examples > example "sine" example index 0 1`] = `
]
`;
exports[`runs examples > example "slice" example index 0 1`] = `
[
"[ 0/1 → 3/16 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 3/16 → 3/8 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]",
"[ 3/8 → 9/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
"[ 9/16 → 21/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 21/32 → 3/4 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 3/4 → 15/16 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ (15/16 → 1/1) ⇝ 9/8 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 15/16 ⇜ (1/1 → 9/8) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 9/8 → 21/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 21/16 → 3/2 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 3/2 → 27/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 27/16 → 15/8 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 15/8 → 63/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ (63/32 → 2/1) ⇝ 33/16 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 63/32 ⇜ (2/1 → 33/16) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 33/16 → 9/4 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 9/4 → 75/32 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 75/32 → 39/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 39/16 → 21/8 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
"[ 21/8 → 45/16 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]",
"[ 45/16 → 3/1 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 3/1 → 51/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 51/16 → 27/8 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 27/8 → 57/16 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 57/16 → 15/4 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 15/4 → 123/32 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
"[ 123/32 → 63/16 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (63/16 → 4/1) ⇝ 33/8 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
]
`;
exports[`runs examples > example "slice" example index 1 1`] = `
[
"[ 0/1 → 1/4 | begin:0 end:0.25 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 1/4 → 1/2 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 1/2 → 3/4 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 3/4 → 1/1 | begin:0.5 end:0.75 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 1/1 → 5/4 | begin:0 end:0.25 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 5/4 → 3/2 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 3/2 → 7/4 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 7/4 → 2/1 | begin:0.75 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 2/1 → 9/4 | begin:0 end:0.25 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 9/4 → 5/2 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 5/2 → 11/4 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 11/4 → 3/1 | begin:0.5 end:0.75 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 3/1 → 13/4 | begin:0 end:0.25 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 13/4 → 7/2 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 7/2 → 15/4 | begin:0.25 end:0.5 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
"[ 15/4 → 4/1 | begin:0.75 _slices:[0 0.25 0.5 0.75] s:breaks125 speed:0.5 unit:c ]",
]
`;
exports[`runs examples > example "slow" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bd ]",
@@ -4350,36 +3999,6 @@ exports[`runs examples > example "speed" example index 1 1`] = `
]
`;
exports[`runs examples > example "splice" example index 0 1`] = `
[
"[ 0/1 → 5/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 5/26 → 5/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 5/13 → 20/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 20/39 → 25/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 25/39 → 10/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 10/13 → 25/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ (25/26 → 1/1) ⇝ 35/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 25/26 ⇜ (1/1 → 35/26) | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 35/26 → 20/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 20/13 → 45/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 45/26 → 25/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ (25/13 → 2/1) ⇝ 80/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 25/13 ⇜ (2/1 → 80/39) | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 80/39 → 85/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 85/39 → 30/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 30/13 → 5/2 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 5/2 → 75/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (75/26 → 3/1) ⇝ 40/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 75/26 ⇜ (3/1 → 40/13) | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]",
"[ 40/13 → 85/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ 85/26 → 45/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
"[ 45/13 → 140/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
"[ 140/39 → 145/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
"[ 145/39 → 50/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
"[ (50/13 → 4/1) ⇝ 105/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
]
`;
exports[`runs examples > example "square" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C3 ]",
@@ -4758,96 +4377,6 @@ exports[`runs examples > example "velocity" example index 0 1`] = `
]
`;
exports[`runs examples > example "vib" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:a vib:0.5 ]",
"[ 1/1 → 2/1 | note:a vib:1 ]",
"[ 2/1 → 3/1 | note:a vib:2 ]",
"[ 3/1 → 4/1 | note:a vib:4 ]",
]
`;
exports[`runs examples > example "vib" example index 1 1`] = `
[
"[ 0/1 → 1/1 | note:a vib:0.5 vibmod:12 ]",
"[ 1/1 → 2/1 | note:a vib:1 vibmod:12 ]",
"[ 2/1 → 3/1 | note:a vib:2 vibmod:12 ]",
"[ 3/1 → 4/1 | note:a vib:4 vibmod:12 ]",
]
`;
exports[`runs examples > example "vibmod" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:a vib:4 vibmod:0.25 ]",
"[ 1/1 → 2/1 | note:a vib:4 vibmod:0.5 ]",
"[ 2/1 → 3/1 | note:a vib:4 vibmod:1 ]",
"[ 3/1 → 4/1 | note:a vib:4 vibmod:2 ]",
]
`;
exports[`runs examples > example "vibmod" example index 1 1`] = `
[
"[ 0/1 → 1/1 | note:a vibmod:0.25 vib:8 ]",
"[ 1/1 → 2/1 | note:a vibmod:0.5 vib:8 ]",
"[ 2/1 → 3/1 | note:a vibmod:1 vib:8 ]",
"[ 3/1 → 4/1 | note:a vibmod:2 vib:8 ]",
]
`;
exports[`runs examples > example "voicing" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:E4 ]",
"[ 0/1 → 1/1 | note:G4 ]",
"[ 0/1 → 1/1 | note:C5 ]",
"[ 1/1 → 2/1 | note:E4 ]",
"[ 1/1 → 2/1 | note:A4 ]",
"[ 1/1 → 2/1 | note:C5 ]",
"[ 2/1 → 3/1 | note:F4 ]",
"[ 2/1 → 3/1 | note:A4 ]",
"[ 2/1 → 3/1 | note:C5 ]",
"[ 3/1 → 4/1 | note:D4 ]",
"[ 3/1 → 4/1 | note:G4 ]",
"[ 3/1 → 4/1 | note:B4 ]",
]
`;
exports[`runs examples > example "voicing" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:64 ]",
"[ 1/8 → 1/4 | note:67 ]",
"[ 1/4 → 3/8 | note:72 ]",
"[ 3/8 → 1/2 | note:76 ]",
"[ 1/2 → 5/8 | note:79 ]",
"[ 5/8 → 3/4 | note:84 ]",
"[ 3/4 → 7/8 | note:88 ]",
"[ 7/8 → 1/1 | note:91 ]",
"[ 1/1 → 9/8 | note:64 ]",
"[ 9/8 → 5/4 | note:69 ]",
"[ 5/4 → 11/8 | note:72 ]",
"[ 11/8 → 3/2 | note:76 ]",
"[ 3/2 → 13/8 | note:81 ]",
"[ 13/8 → 7/4 | note:84 ]",
"[ 7/4 → 15/8 | note:88 ]",
"[ 15/8 → 2/1 | note:93 ]",
"[ 2/1 → 17/8 | note:65 ]",
"[ 17/8 → 9/4 | note:69 ]",
"[ 9/4 → 19/8 | note:72 ]",
"[ 19/8 → 5/2 | note:77 ]",
"[ 5/2 → 21/8 | note:81 ]",
"[ 21/8 → 11/4 | note:84 ]",
"[ 11/4 → 23/8 | note:89 ]",
"[ 23/8 → 3/1 | note:93 ]",
"[ 3/1 → 25/8 | note:62 ]",
"[ 25/8 → 13/4 | note:67 ]",
"[ 13/4 → 27/8 | note:71 ]",
"[ 27/8 → 7/2 | note:74 ]",
"[ 7/2 → 29/8 | note:79 ]",
"[ 29/8 → 15/4 | note:83 ]",
"[ 15/4 → 31/8 | note:86 ]",
"[ 31/8 → 4/1 | note:91 ]",
]
`;
exports[`runs examples > example "voicings" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:B3 ]",
@@ -4886,6 +4415,31 @@ exports[`runs examples > example "vowel" example index 0 1`] = `
]
`;
exports[`runs examples > example "webdirt" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd n:0 ]",
"[ 1/8 → 1/4 | s:bd n:0 ]",
"[ 1/4 → 1/2 | s:hh n:0 ]",
"[ 1/2 → 3/4 | s:sd n:0 ]",
"[ 3/4 → 1/1 | s:hh n:0 ]",
"[ 1/1 → 9/8 | s:bd n:1 ]",
"[ 9/8 → 5/4 | s:bd n:1 ]",
"[ 5/4 → 3/2 | s:hh n:1 ]",
"[ 3/2 → 7/4 | s:sd n:1 ]",
"[ 7/4 → 2/1 | s:hh n:1 ]",
"[ 2/1 → 17/8 | s:bd n:0 ]",
"[ 17/8 → 9/4 | s:bd n:0 ]",
"[ 9/4 → 5/2 | s:hh n:0 ]",
"[ 5/2 → 11/4 | s:sd n:0 ]",
"[ 11/4 → 3/1 | s:hh n:0 ]",
"[ 3/1 → 25/8 | s:bd n:1 ]",
"[ 25/8 → 13/4 | s:bd n:1 ]",
"[ 13/4 → 7/2 | s:hh n:1 ]",
"[ 7/2 → 15/4 | s:sd n:1 ]",
"[ 15/4 → 4/1 | s:hh n:1 ]",
]
`;
exports[`runs examples > example "when" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:c3 ]",
File diff suppressed because it is too large Load Diff
-1
View File
@@ -60,7 +60,6 @@ const toneHelpersMocked = {
vol: mockNode,
out: id,
osc: id,
samples: id,
adsr: id,
getDestination: id,
players: mockNode,
-2
View File
@@ -34,11 +34,9 @@
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"@strudel/desktopbridge": "workspace:*",
"@supabase/supabase-js": "^2.21.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@tauri-apps/api": "^1.4.0",
"@types/node": "^18.16.3",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
This super cool font is by 3d@galax.xyz https://galax.xyz/TELETEXT/
Binary file not shown.
-121
View File
@@ -1,121 +0,0 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
+1 -8
View File
@@ -46,14 +46,12 @@ const base = BASE_URL;
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
<script>
import { settings, injectStyle } from '../repl/themes.mjs';
import { settings } from '../repl/themes.mjs';
import { settingsMap } from '../settings.mjs';
import { listenKeys } from 'nanostores';
const themeStyle = document.createElement('style');
themeStyle.id = 'strudel-theme';
document.head.append(themeStyle);
let resetThemeStyle;
function activateTheme(name) {
if (!settings[name]) {
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
@@ -72,11 +70,6 @@ const base = BASE_URL;
} else {
document.documentElement.classList.add('dark');
}
resetThemeStyle?.();
resetThemeStyle = undefined;
if (themeSettings.customStyle) {
resetThemeStyle = injectStyle(themeSettings.customStyle);
}
}
activateTheme(settingsMap.get().theme);
@@ -47,11 +47,10 @@ const sidebar = SIDEBAR[langCode];
</nav>
<script is:inline>
window.addEventListener('DOMContentLoaded', () => {
var target = document.querySelector('[aria-current="page"]');
const nav = document.querySelector('.nav-groups');
if (nav && target && target.offsetTop > window.innerHeight - 100) {
nav.scrollTop = target.offsetTop;
}
});
window.addEventListener('DOMContentLoaded', () => {
var target = document.querySelector('[aria-current="page"]');
if (target && target.offsetTop > window.innerHeight - 100) {
document.querySelector('.nav-groups').scrollTop = target.offsetTop;
}
});
</script>
-268
View File
@@ -1,268 +0,0 @@
import useEvent from '@strudel.cycles/react/src/hooks/useEvent.mjs';
import useFrame from '@strudel.cycles/react/src/hooks/useFrame.mjs';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { midi2note } from '@strudel.cycles/core';
import { useState, useRef, useEffect } from 'react';
import Claviature from '@components/Claviature';
let Button = (props) => <button {...props} className="bg-lineHighlight p-2 rounded-md color-foreground" />;
function plotValues(ctx, values, min, max, color) {
let { width, height } = ctx.canvas;
ctx.strokeStyle = color;
const thickness = 8;
ctx.lineWidth = thickness;
ctx.beginPath();
let x = (f) => ((f - min) / (max - min)) * width;
let y = (i) => (1 - i / values.length) * height;
values.forEach((f, i, a) => {
ctx.lineTo(x(f), y(i));
});
ctx.stroke();
}
function getColor(cssVariable) {
if (typeof document === 'undefined') {
return 'white';
}
const dummyElement = document.createElement('div');
dummyElement.style.color = cssVariable;
// Append the dummy element to the document body
document.body.appendChild(dummyElement);
// Get the computed style of the dummy element
const styles = getComputedStyle(dummyElement);
// Get the value of the CSS variable
const color = styles.getPropertyValue(cssVariable);
document.body.removeChild(dummyElement);
return color;
}
let pitchColor = '#eab308';
let frequencyColor = '#3b82f6';
export function PitchSlider({
buttons = [],
animatable = false,
plot = false,
showPitchSlider = false,
showFrequencySlider = false,
pitchStep = '0.001',
min = 55,
max = 7040,
initial = 220,
baseFrequency = min,
zeroOffset = 0,
claviature,
}) {
const oscRef = useRef();
const activeRef = useRef();
const freqRef = useRef(initial);
const historyRef = useRef([freqRef.current]);
const frameRef = useRef();
const canvasRef = useRef();
const [hz, setHz] = useState(freqRef.current);
useEffect(() => {
freqRef.current = hz;
}, [hz]);
useEvent('mouseup', () => {
oscRef.current?.stop();
activeRef.current = false;
});
let freqSlider2freq = (progress) => min + progress * (max - min);
let pitchSlider2freq = (progress) => min * 2 ** (progress * Math.log2(max / min));
let freq2freqSlider = (freq) => (freq - min) / (max - min);
let freq2pitchSlider = (freq) => {
const [minOct, maxOct] = [Math.log2(min), Math.log2(max)];
return (Math.log2(freq) - minOct) / (maxOct - minOct);
};
const freqSlider = freq2freqSlider(hz);
const pitchSlider = freq2pitchSlider(hz);
let startOsc = (hz) => {
if (oscRef.current) {
oscRef.current.stop();
}
oscRef.current = getAudioContext().createOscillator();
oscRef.current.frequency.value = hz;
oscRef.current.connect(getAudioContext().destination);
oscRef.current.start();
activeRef.current = true;
setHz(hz);
};
let startSweep = (exp = false) => {
let f = min;
startOsc(f);
const frame = () => {
if (f < max) {
if (!exp) {
f += 10;
} else {
f *= 1.01;
}
oscRef.current.frequency.value = f;
frameRef.current = requestAnimationFrame(frame);
} else {
oscRef.current.stop();
cancelAnimationFrame(frameRef.current);
}
setHz(f);
};
requestAnimationFrame(frame);
};
useFrame(() => {
historyRef.current.push(freqRef.current);
historyRef.current = historyRef.current.slice(-1000);
if (canvasRef.current) {
let ctx = canvasRef.current.getContext('2d');
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
if (showFrequencySlider) {
plotValues(ctx, historyRef.current, min, max, frequencyColor);
}
if (showPitchSlider) {
const [minOct, maxOct] = [Math.log2(min), Math.log2(max)];
let perceptual = historyRef.current.map((v) => Math.log2(v));
plotValues(ctx, perceptual, minOct, maxOct, pitchColor);
}
}
}, plot);
let handleChangeFrequency = (f) => {
setHz(f);
if (oscRef.current) {
oscRef.current.frequency.value = f;
}
};
let handleMouseDown = () => {
cancelAnimationFrame(frameRef.current);
startOsc(hz);
};
let exponent, activeNote, activeNoteLabel;
if (showPitchSlider) {
const expOffset = baseFrequency ? Math.log2(baseFrequency / min) : 0;
exponent = freq2pitchSlider(hz) * Math.log2(max / min) - expOffset;
let semitones = parseFloat((exponent * 12).toFixed(2));
if (zeroOffset) {
semitones = semitones + zeroOffset;
const isWhole = Math.round(semitones) === semitones;
activeNote = midi2note(Math.round(semitones));
activeNoteLabel = (!isWhole ? '~' : '') + activeNote;
semitones = !isWhole ? semitones.toFixed(2) : semitones;
exponent = (
<>
(<span className="text-yellow-500">{semitones}</span> - {zeroOffset})/12
</>
);
} else if (semitones % 12 === 0) {
exponent = <span className="text-yellow-500">{semitones / 12}</span>;
} else if (semitones % 1 === 0) {
exponent = (
<>
<span className="text-yellow-500">{semitones}</span>/12
</>
);
} else {
exponent = <span className="text-yellow-500">{exponent.toFixed(2)}</span>;
}
}
return (
<>
<span className="font-mono">
{showFrequencySlider && <span className="text-blue-500">{hz.toFixed(0)}Hz</span>}
{showFrequencySlider && showPitchSlider && <> = </>}
{showPitchSlider && (
<>
{baseFrequency || min}Hz * 2<sup>{exponent}</sup>
</>
)}
</span>
{claviature && (
<>
{' '}
= <span className="text-yellow-500">{activeNoteLabel}</span>
</>
)}
<div>
{showFrequencySlider && (
<div className="flex space-x-1 items-center">
<input
type="range"
value={freqSlider}
min={0}
max={1}
step={0.001}
onMouseDown={handleMouseDown}
className={`block w-full max-w-[600px] accent-blue-500 `}
onChange={(e) => {
const f = freqSlider2freq(parseFloat(e.target.value));
handleChangeFrequency(f);
}}
/>
</div>
)}
{showPitchSlider && (
<div>
<input
type="range"
value={pitchSlider}
min={0}
max={1}
//step=".001"
step={pitchStep}
onMouseDown={handleMouseDown}
className={`block w-full max-w-[600px] accent-yellow-500`}
onChange={(e) => {
const f = pitchSlider2freq(parseFloat(e.target.value));
handleChangeFrequency(f);
}}
/>
</div>
)}
</div>
<div className="px-2">
{plot && <canvas ref={canvasRef} className="w-full max-w-[584px] h-[300px]" height="600" width={800} />}
</div>
<div className="space-x-2">
{animatable && (
<Button onClick={() => startSweep()}>
<span style={{ color: '#3b82f6' }}>Frequency Sweep</span>
</Button>
)}
{animatable && (
<Button onClick={() => startSweep(true)}>
<span style={{ color: '#eab308' }}>Pitch Sweep</span>
</Button>
)}
{buttons.map((f, i) => (
<Button key={(f, i)} onMouseDown={() => startOsc(f)}>
{f}Hz
</Button>
))}
</div>
{claviature && (
<Claviature
onMouseDown={(note) => {
const f = 440 * 2 ** ((note - 69) / 12);
handleChangeFrequency(f);
cancelAnimationFrame(frameRef.current);
startOsc(f);
}}
options={{
range: ['A1', 'A5'],
scaleY: 0.75,
scaleX: 0.86,
colorize: activeNote ? [{ keys: [activeNote], color: '#eab308' }] : [],
labels: activeNote ? { [activeNote]: activeNote } : {},
}}
/>
)}
</>
);
}
-1
View File
@@ -89,7 +89,6 @@ export const SIDEBAR: Sidebar = {
{ text: 'Accumulation', link: 'learn/accumulation' },
{ text: 'Tonal Functions', link: 'learn/tonal' },
],
Understand: [{ text: 'Pitch', link: 'understand/pitch' }],
Development: [
{ text: 'REPL', link: 'technical-manual/repl' },
{ text: 'Sounds', link: 'technical-manual/sounds' },
+4 -5
View File
@@ -12,11 +12,10 @@ export function JsDoc({ name, h = 3, hideDescription, punchcard, canvasHeight })
}
const synonyms = getTag('synonyms', item)?.split(', ') || [];
const CustomHeading = `h${h}`;
const description =
item.description?.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => {
// console.log(_, 'a', a, 'b', b);
return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
}) || '';
const description = item.description.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => {
// console.log(_, 'a', a, 'b', b);
return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
});
return (
<>
{!!h && <CustomHeading>{item.longname}</CustomHeading>}
-56
View File
@@ -49,10 +49,6 @@ Each filter has 2 parameters:
<JsDoc client:idle name="bpq" h={0} />
## ftype
<JsDoc client:idle name="ftype" h={0} />
## vowel
<JsDoc client:idle name="vowel" h={0} />
@@ -82,58 +78,6 @@ Strudel uses ADSR envelopes, which are probably the most common way to describe
<JsDoc client:idle name="release" h={0} />
# Filter Envelope
Each filter can receive an additional filter envelope controlling the cutoff value dynamically. It uses an ADSR envelope similar to the one used for amplitude. There is an additional parameter to control the depth of the filter modulation: `lpenv`|`hpenv`|`bpenv`. This allows you to play subtle or huge filter modulations just the same by only increasing or decreasing the depth.
<MiniRepl
client:idle
tune={`note("[c eb g <f bb>](3,8,<0 1>)".sub(12))
.s("<sawtooth>/64")
.lpf(sine.range(500,3000).slow(16))
.lpa(0.005)
.lpd(perlin.range(.02,.2))
.lps(perlin.range(0,.5).slow(3))
.lpq(sine.range(2,10).slow(32))
.release(.5)
.lpenv(perlin.range(1,8).slow(2))
.ftype('24db')
.room(1)
.juxBy(.5,rev)
.sometimes(add(note(12)))
.stack(s("bd*2").bank('RolandTR909'))
.gain(.5)`}
/>
There is one filter envelope for each filter type and thus one set of envelope filter parameters preceded either by `lp`, `hp` or `bp`:
- `lpattack`, `lpdecay`, `lpsustain`, `lprelease`, `lpenv`: filter envelope for the lowpass filter.
- alternatively: `lpa`, `lpd`, `lps`, `lpr` and `lpe`.
- `hpattack`, `hpdecay`, `hpsustain`, `hprelease`, `hpenv`: filter envelope for the highpass filter.
- alternatively: `hpa`, `hpd`, `hps`, `hpr` and `hpe`.
- `bpattack`, `bpdecay`, `bpsustain`, `bprelease`, `bpenv`: filter envelope for the bandpass filter.
- alternatively: `bpa`, `bpd`, `bps`, `bpr` and `bpe`.
## lpattack
<JsDoc client:idle name="lpattack" h={0} />
## lpdecay
<JsDoc client:idle name="lpdecay" h={0} />
## lpsustain
<JsDoc client:idle name="lpsustain" h={0} />
## lprelease
<JsDoc client:idle name="lprelease" h={0} />
## lpenv
<JsDoc client:idle name="lpenv" h={0} />
# Dynamics
## gain
-24
View File
@@ -303,18 +303,6 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="Pattern.end" h={0} />
### loop
<JsDoc client:idle name="loop" h={0} />
### loopBegin
<JsDoc client:idle name="loopBegin" h={0} />
### loopEnd
<JsDoc client:idle name="loopEnd" h={0} />
### cut
<JsDoc client:idle name="cut" h={0} />
@@ -327,22 +315,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="Pattern.loopAt" h={0} />
### fit
<JsDoc client:idle name="fit" h={0} />
### chop
<JsDoc client:idle name="Pattern.chop" h={0} />
### slice
<JsDoc client:idle name="Pattern.slice" h={0} />
### splice
<JsDoc client:idle name="splice" h={0} />
### speed
<JsDoc client:idle name="speed" h={0} />
+12 -126
View File
@@ -8,138 +8,24 @@ import { JsDoc } from '../../docs/JsDoc';
# Synths
In addition to the sampling engine, strudel comes with a synthesizer to create sounds on the fly.
For now, [samples](/learn/samples) are the main way to play with Strudel.
In the future, more powerful synthesis capabilities will be added.
If in the meantime you want to dive deeper into audio synthesis with Tidal, you will need to [install SuperCollider and SuperDirt](https://tidalcycles.org/docs/).
## Basic Waveforms
## Playing synths with `s`
The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can be selected via `sound` (or `s`):
We can change the sound, using the `s` function:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.sound("<sawtooth square triangle sine>")
.scope()`}
/>
<MiniRepl client:idle tune={`note("c2 <eb2 <g2 g1>>").s('sawtooth')`} />
If you don't set a `sound` but a `note` the default value for `sound` is `triangle`!
Here, we are wrapping our notes inside `note` and set the sound using `s`, connected by a dot.
### Additive Synthesis
Those functions are only 2 of many ways to alter the properties, or _params_ of a sound.
The power of patterns allows us to sequence any _param_ independently:
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
<MiniRepl client:idle tune={`note("c2 <eb2 <g2 g1>>").s("<sawtooth square triangle>")`} />
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.sound("sawtooth")
.n("<32 16 8 4>")
.scope()`}
/>
When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting.
You can also set `n` directly in mini notation with `sound`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.sound("sawtooth:<32 16 8 4>")
.scope()`}
/>
Note for tidal users: `n` in tidal is synonymous to `note` for synths only.
In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms.
## Vibrato
### vib
<JsDoc client:idle name="vib" h={0} />
### vibmod
<JsDoc client:idle name="vibmod" h={0} />
## FM Synthesis
FM Synthesis is a technique that changes the frequency of a basic waveform rapidly to alter the timbre.
You can use fm with any of the above waveforms, although the below examples all use the default triangle wave.
### fm
<JsDoc client:idle name="fm" h={0} />
### fmh
<JsDoc client:idle name="fmh" h={0} />
### fmattack
<JsDoc client:idle name="fmattack" h={0} />
### fmdecay
<JsDoc client:idle name="fmdecay" h={0} />
### fmsustain
<JsDoc client:idle name="fmsustain" h={0} />
### fmenv
<JsDoc client:idle name="fmenv" h={0} />
## Wavetable Synthesis
Strudel can also use the sampler to load custom waveforms as a replacement of the default waveforms used by WebAudio for the base synth. A default set of more than 1000 wavetables is accessible by default (coming from the [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) set). You can also import/use your own. A wavetable is a one-cycle waveform, which is then repeated to create a sound at the desired frequency. It is a classic but very effective synthesis technique.
Any sample preceded by the `wt_` prefix will be loaded as a wavetable. This means that the `loop` argument will be set to `1` by defalt. You can scan over the wavetable by using `loopBegin` and `loopEnd` as well.
<MiniRepl
client:idle
tune={`samples('github:Bubobubobubobubo/Dough-Waveforms/main/');
note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")
.n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9)
.s('wt_flute').velocity(0.25).often(n => n.ply(2))
.release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0)
.cutoff(2000).scope({}).cutoff("<1000 2000 4000>").fast(2)`}
/>
## ZZFX
The "Zuper Zmall Zound Zynth" [ZZFX](https://github.com/KilledByAPixel/ZzFX) is also integrated in strudel.
Developed by [Frank Force](https://frankforce.com/), it is a synth and FX engine originally intended to be used for size coding games.
It has 20 parameters in total, here is a snippet that uses all:
<MiniRepl
client:idle
tune={`note("<c2 eb2 f2 g2>") // also supports freq
.s("<z_sawtooth z_tan z_noise z_sine z_square>")
.zrand(0) // randomization
// zzfx envelope
.attack(0.001)
.decay(0.1)
.sustain(.8)
.release(.1)
// special zzfx params
.curve(1) // waveshape 1-3
.slide(0) // +/- pitch slide
.deltaSlide(0) // +/- pitch slide (?)
.noise(0) // make it dirty
.zmod(0) // fm speed
.zcrush(0) // bit crush 0 - 1
.zdelay(0) // simple delay
.pitchJump(0) // +/- pitch change after pitchJumpTime
.pitchJumpTime(0) // >0 time after pitchJump is applied
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
.tremolo(0) // 0-1 lfo volume modulation amount
//.duration(.2) // overwrite strudel event duration
//.gain(1) // change volume
.scope() // vizualise waveform (not zzfx related)
`}
/>
Note that you can also combine zzfx with all the other audio fx (next chapter).
Now we not only pattern the notes, but the sound as well!
`sawtooth` `square` and `triangle` are the basic waveforms available in `s`.
Next up: [Audio Effects](/learn/effects)...
+12 -5
View File
@@ -10,13 +10,20 @@ import { JsDoc } from '../../docs/JsDoc';
These functions use [tonaljs](https://github.com/tonaljs/tonal) to provide helpers for musical operations.
### voicing()
<JsDoc client:idle name="voicing" h={0} />
### scale(name)
<JsDoc client:idle name="scale" h={0} />
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranspose.
<MiniRepl
client:idle
tune={`"0 2 4 6 4 2"
.scale("C2:major C2:minor").slow(2))
.note().s("piano")`}
/>
Note that the scale root is octaved here. You can also omit the octave, then index zero will default to octave 3.
All the available scale names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
### transpose(semitones)
-169
View File
@@ -1,169 +0,0 @@
---
title: Understanding Pitch
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { PitchSlider } from '../../components/PitchSlider';
import Box from '@components/Box.astro';
# Understanding Pitch
Let's learn how pitch works! The slider below controls the <span style="color:#3b82f6;">frequency</span> of an oscillator, producing a pitch:
{/* <PitchSlider client:load showFrequencySlider plot /> */}
<PitchSlider client:load showFrequencySlider min={20} max={20000} />
- Drag the slider to hear a pitch
- Move the slider to change the pitch
- Observe how the Hz number changes
- <span className="text-red-300">Caution</span>: The higher frequencies could be disturbing for children or animals!
The Hz number is the frequency of the pitch you're hearing.
The higher the frequency, the higher the pitch and vice versa.
A pitch occurs whenever something is vibrating / oscillating at a frequency, in this case it's your speaker.
The unit **Hz** describes how many times that oscillation happens per second.
Our eyes are too slow to actually see the oscillation on the speaker, but we can <a href="https://www.youtube.com/watch?v=CDMBWw7OuJQ" target="_blank">see it in slow motion</a>.
<Box>
The hearing range of a newborn is said to be between 20Hz and 20000Hz.
The upper limit decreases with age. What's your upper limit?
</Box>
In Strudel, we can play frequencies directly with the `freq` control:
<MiniRepl client:visible tune={`freq("200 [300,500] 400 [500,<600 670 712 670>]")`} />
## Frequency vs Pitch Perception
Maybe you have already noticed that the <span style="color:#3b82f6;">frequency slider</span> is "lopsided",
meaning the pitch changes more in the left region and less in the right region.<br/>
To make that more obvious, let's add a <span style="color:#eab308">pitch slider</span>
that controls the frequency on a different scale:
<PitchSlider animatable plot showFrequencySlider showPitchSlider client:load />
Try out the buttons above to sweep through the frequency range in 2 different ways:
- Frequency Sweep: <span style="color:#3b82f6;">frequency rises linear</span> , <span style="color:#eab308">pitch rises logarithmic</span>
- Pitch Sweep: <span style="color:#3b82f6;">frequency rises exponential</span> , <span style="color:#eab308">pitch rises linear</span>
<Box>
Don't be scared of these mathematical terms:
- "logarithmic" is just a fancy way of saying "it starts fast and slows down"
- "exponential" is just a fancy way of saying "it starts slow and gets faster"
</Box>
Most of the time, we might want to control pitch in a way that matches our perception,
which is what the <span style="color:#eab308">pitch slider</span> does.
## From Hz to Semitones
Because Hz does not match our perception, let's try to find a unit for pitch that matches.
To approach that unit of pitch, let's look at how frequency behaves when it is doubled:
<PitchSlider client:load showPitchSlider showFrequencySlider pitchStep={1 / 7} />
- Use the now stepped pitch slider above
- Can you hear how these pitches seem related to each other?
<Box>
In musical terms, a pitch with double the frequency of another is an `octave` higher.
</Box>
Because octaves are pretty far apart, octaves are typically divided into 12 smaller parts:
<PitchSlider client:load showPitchSlider showFrequencySlider pitchStep={1 / 12} min={440} max={880} initial={440} />
This step is also called a semitone, which is the most common division of pitched music.
For example, the keys on a piano keyboard are also divided into semitones.
In Strudel, we could do that with `freq` like this:
<MiniRepl
client:visible
tune={`freq(
"0 4 7 12"
.fmap(n => 440 * 2**(n/12))
)`}
/>
Of course, this can be written shorter with note, as we will see below.
## From Semitones to MIDI numbers
Now we know what the distance of a semitone is.
Above, we used an arbitrary base frequency of 440Hz, which means the exponent 0 is equal to 440Hz.
Typically, 440Hz is standardized to the number 69, which leads to this calculation:
<PitchSlider
client:load
showPitchSlider
showFrequencySlider
baseFrequency={440}
zeroOffset={69}
pitchStep={1 / 12 / 7}
min={440 / 8}
max={7040}
initial={440}
/>
The yellow number is now a MIDI number, covering more than the whole human hearing range with numbers from 0 to 127.
In Strudel, we can use MIDI numbers inside `note`:
<MiniRepl client:visible tune={`note("69 73 76 81")`} />
## From MIDI numbers to notes
In western music theory, notes are used instead of numbers.
For each midi number, there is at least one note label:
<PitchSlider
client:load
showPitchSlider
showFrequencySlider
baseFrequency={440}
zeroOffset={69}
pitchStep={1 / 48}
min={440 / 8}
max={880}
initial={440}
claviature
/>
A full note label consists of a letter (A-G), 0 or more accidentals (b | #) and an octave number.
This system is also known as [Scientific Pitch Notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation).
In Strudel, these note labels can also be used inside `note` as an alternative to midi numbers:
<MiniRepl client:visible tune={`note("A4 C#5 E5 A5").piano()`} />
## Open Questions
Now that we have learned about different representations of pitch, there are still open questions:
- Why 12 notes? What about different divisions of the octave?
- Why are notes labeled as they are? Why only 7 letters?
- Are there other labeling systems?
- What about Just Intonation Systems?
- What about Timbre?
All those questions are important to ask and will be answered in another article.
## Definition
At first, I wanted to start this article with a definition, but then thought it might be a good idea to focus on intuitive exploration.
Maybe you now understand this definition much better:
<Box>
From [wikipedia](<https://en.wikipedia.org/wiki/Pitch_(music)>): "Pitch is a perceptual property of sounds that allows their ordering on a frequency-related scale, or more commonly, pitch is the quality that makes it possible to judge sounds as "higher" and "lower" in the sense associated with musical melodies."
</Box>
+20 -40
View File
@@ -16,7 +16,7 @@ const TAURI = window.__TAURI__;
export function Footer({ context }) {
const footerContent = useRef();
const [log, setLog] = useState([]);
const { activeFooter, isZen, panelPosition } = useSettings();
const { activeFooter, isZen } = useSettings();
useLayoutEffect(() => {
if (footerContent.current && activeFooter === 'console') {
@@ -71,15 +71,8 @@ export function Footer({ context }) {
if (isZen) {
return null;
}
const isActive = activeFooter !== '';
let positions = {
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
};
return (
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}>
<footer className="bg-lineHighlight z-[20]">
<div className="flex justify-between px-2">
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
<FooterTab name="intro" label="welcome" />
@@ -90,24 +83,22 @@ export function Footer({ context }) {
{TAURI && <FooterTab name="files" />}
</div>
{activeFooter !== '' && (
<button onClick={() => setActiveFooter('')} className="text-foreground px-2" aria-label="Close Panel">
<button onClick={() => setActiveFooter('')} className="text-foreground" aria-label="Close Panel">
<XMarkIcon className="w-5 h-5" />
</button>
)}
</div>
{activeFooter !== '' && (
<div className="relative overflow-hidden">
<div className="text-white overflow-auto h-full max-w-full" ref={footerContent}>
{activeFooter === 'intro' && <WelcomeTab />}
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab scheduler={context.scheduler} />}
{activeFooter === 'files' && <FilesTab />}
</div>
<div className="text-white flex-none h-[360px] overflow-auto max-w-full relative" ref={footerContent}>
{activeFooter === 'intro' && <WelcomeTab />}
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab scheduler={context.scheduler} />}
{activeFooter === 'files' && <FilesTab />}
</div>
)}
</nav>
</footer>
);
}
@@ -375,8 +366,6 @@ const fontFamilyOptions = {
'we-come-in-peace': 'we-come-in-peace',
FiraCode: 'FiraCode',
'FiraCode-SemiBold': 'FiraCode SemiBold',
teletext: 'teletext',
mode7: 'mode7',
};
function SettingsTab({ scheduler }) {
@@ -388,7 +377,6 @@ function SettingsTab({ scheduler }) {
isLineWrappingEnabled,
fontSize,
fontFamily,
panelPosition,
} = useSettings();
return (
@@ -432,21 +420,14 @@ function SettingsTab({ scheduler }) {
/>
</FormItem>
</div>
<FormItem label="Keybindings">
<ButtonGroup
value={keybindings}
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
></ButtonGroup>
</FormItem>
<FormItem label="Panel Position">
<ButtonGroup
value={panelPosition}
onChange={(value) => settingsMap.setKey('panelPosition', value)}
items={{ bottom: 'Bottom', right: 'Right' }}
></ButtonGroup>
</FormItem>
<FormItem label="Code Settings">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<FormItem label="Keybindings">
<ButtonGroup
value={keybindings}
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
></ButtonGroup>
</FormItem>
<Checkbox
label="Display line numbers"
onChange={(cbEvent) => settingsMap.setKey('isLineNumbersDisplayed', cbEvent.target.checked)}
@@ -462,8 +443,7 @@ function SettingsTab({ scheduler }) {
onChange={(cbEvent) => settingsMap.setKey('isLineWrappingEnabled', cbEvent.target.checked)}
value={isLineWrappingEnabled}
/>
</FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
</div>
<FormItem label="Reset Settings">
<button
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"

Some files were not shown because too many files have changed in this diff Show More