Merge branch 'main' into patterns-tab
This commit is contained in:
commit
6c3173f45b
76 changed files with 2981 additions and 531 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "0.8.4",
|
||||
"version": "0.9.0",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -118,7 +118,10 @@ const generic_params = [
|
|||
* @name fmh
|
||||
* @param {number | Pattern} harmonicity
|
||||
* @example
|
||||
* note("c e g b").fm(4).fmh("<1 2 1.5 1.61>")
|
||||
* note("c e g b")
|
||||
* .fm(4)
|
||||
* .fmh("<1 2 1.5 1.61>")
|
||||
* .scope()
|
||||
*
|
||||
*/
|
||||
[['fmh', 'fmi'], 'fmh'],
|
||||
|
|
@ -130,10 +133,72 @@ const generic_params = [
|
|||
* @param {number | Pattern} brightness modulation index
|
||||
* @synonyms fmi
|
||||
* @example
|
||||
* note("c e g b").fm("<0 1 2 8 32>")
|
||||
* 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`.
|
||||
|
|
@ -146,6 +211,9 @@ 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.
|
||||
|
|
@ -231,16 +299,52 @@ const generic_params = [
|
|||
*/
|
||||
['end'],
|
||||
/**
|
||||
* Loops the sample (from `begin` to `end`) the specified number of times.
|
||||
* Loops the sample.
|
||||
* 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} times How often the sample is looped
|
||||
* @param {number | Pattern} on If 1, the sample is looped
|
||||
* @example
|
||||
* s("bd").loop("<1 2 3 4>").osc()
|
||||
* s("casio").loop(1)
|
||||
*
|
||||
*/
|
||||
['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
|
||||
/**
|
||||
|
|
@ -255,15 +359,6 @@ 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
|
||||
|
|
@ -275,7 +370,6 @@ const generic_params = [
|
|||
*
|
||||
*/
|
||||
['coarse'],
|
||||
|
||||
/**
|
||||
* choose the channel the pattern is sent to in superdirt
|
||||
*
|
||||
|
|
@ -309,6 +403,227 @@ 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.
|
||||
*
|
||||
|
|
@ -325,6 +640,36 @@ 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.
|
||||
|
|
@ -503,6 +848,9 @@ const generic_params = [
|
|||
*
|
||||
*/
|
||||
['lsize'],
|
||||
// label for pianoroll
|
||||
['activeLabel'],
|
||||
[['label', 'activeLabel']],
|
||||
// ['lfo'],
|
||||
// ['lfocutoffint'],
|
||||
// ['lfodelay'],
|
||||
|
|
@ -796,8 +1144,22 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -9,25 +9,29 @@ 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;
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.width = window.innerWidth * scale;
|
||||
canvas.height = window.innerHeight * scale;
|
||||
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;
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.width = window.innerWidth * scale;
|
||||
canvas.height = window.innerHeight * scale;
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
};
|
||||
|
||||
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
||||
if (typeof window === 'undefined') {
|
||||
return this;
|
||||
}
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
|
|
@ -59,7 +63,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||
|
||||
export const cleanupDraw = (clearScreen = true) => {
|
||||
const ctx = getDrawContext();
|
||||
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -2273,14 +2273,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,9 +2307,16 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
|
|||
return _loopAt(factor, pat, 1);
|
||||
});
|
||||
|
||||
// this function will be redefined in repl.mjs to use the correct cps value.
|
||||
// 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) => ({
|
||||
|
|
|
|||
|
|
@ -29,138 +29,26 @@ const getValue = (e) => {
|
|||
return value;
|
||||
};
|
||||
|
||||
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;
|
||||
Pattern.prototype.pianoroll = function (options = {}) {
|
||||
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
||||
|
||||
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, events, t) => {
|
||||
ctx.fillStyle = background;
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
if (!smear) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
(ctx, haps, t) => {
|
||||
const inFrame = (event) =>
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||
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);
|
||||
}
|
||||
pianoroll({
|
||||
...options,
|
||||
time: t,
|
||||
ctx,
|
||||
haps: haps.filter(inFrame),
|
||||
});
|
||||
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;
|
||||
|
|
@ -191,6 +79,13 @@ 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;
|
||||
|
|
@ -234,58 +129,75 @@ 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);
|
||||
}
|
||||
/* 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.endClipped > 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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
// draw playhead
|
||||
|
|
@ -311,11 +223,15 @@ export function getDrawOptions(drawTime, options = {}) {
|
|||
}
|
||||
|
||||
Pattern.prototype.punchcard = function (options) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
|
||||
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...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 }) }),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
3
packages/desktopbridge/README.md
Normal file
3
packages/desktopbridge/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# @strudel/desktopbridge
|
||||
|
||||
This package contains utilities used to communicate with the Tauri backend
|
||||
9
packages/desktopbridge/index.mjs
Normal file
9
packages/desktopbridge/index.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
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
packages/desktopbridge/loggerbridge.mjs
Normal file
11
packages/desktopbridge/loggerbridge.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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
packages/desktopbridge/midibridge.mjs
Normal file
52
packages/desktopbridge/midibridge.mjs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
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
packages/desktopbridge/oscbridge.mjs
Normal file
43
packages/desktopbridge/oscbridge.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
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
packages/desktopbridge/package.json
Normal file
29
packages/desktopbridge/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"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
packages/desktopbridge/utils.mjs
Normal file
4
packages/desktopbridge/utils.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
|
||||
export const Invoke = invoke;
|
||||
export const isTauri = () => window.__TAURI_IPC__ != null;
|
||||
|
|
@ -78,7 +78,6 @@ function getDevice(output, outputs) {
|
|||
return IACOutput ?? outputs[0];
|
||||
}
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output) {
|
||||
if (isPattern(output)) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "React components for strudel",
|
||||
"main": "src/index.js",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -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.whole.end >= phase - lookbehind - lookahead) // in frame
|
||||
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) // in frame
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
onDraw(pattern, phase - lookahead, visibleHaps.current, drawTime);
|
||||
}, [pattern]),
|
||||
}, [pattern, onDraw]),
|
||||
);
|
||||
useEffect(() => {
|
||||
if (started) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ function useStrudel({
|
|||
canvasId,
|
||||
drawContext,
|
||||
drawTime = [-2, 2],
|
||||
paintOptions = {},
|
||||
}) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
canvasId = canvasId || `canvas-${id}`;
|
||||
|
|
@ -88,9 +89,9 @@ function useStrudel({
|
|||
(pattern, time, haps, drawTime) => {
|
||||
const { onPaint } = pattern.context || {};
|
||||
const ctx = typeof drawContext === 'function' ? drawContext(canvasId) : drawContext;
|
||||
onPaint?.(ctx, time, haps, drawTime);
|
||||
onPaint?.(ctx, time, haps, drawTime, paintOptions);
|
||||
},
|
||||
[drawContext, canvasId],
|
||||
[drawContext, canvasId, paintOptions],
|
||||
);
|
||||
|
||||
const drawFirstFrame = useCallback(
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ 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';
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const settings = {
|
|||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#0f380f',
|
||||
light: true,
|
||||
customStyle: '.cm-line { line-height: 1 }',
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'light',
|
||||
|
|
@ -35,5 +36,6 @@ 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' },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,5 +33,6 @@ 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' },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,5 +36,6 @@ 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,5 +40,6 @@ export default createTheme({
|
|||
|
||||
{ tag: t.className, color: '#decb6b' },
|
||||
{ tag: t.invalid, color: '#ffffff' },
|
||||
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
50
packages/react/src/themes/teletext.js
Normal file
50
packages/react/src/themes/teletext.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -161,5 +161,6 @@ Then just make sure your first call of `superdough` happens after a click of som
|
|||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { getAudioContext } from './superdough.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
|
||||
export function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
|
|
@ -6,17 +7,6 @@ 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();
|
||||
|
|
@ -39,6 +29,22 @@ 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);
|
||||
|
|
@ -61,10 +67,48 @@ export const getADSR = (attack, decay, sustain, release, velocity, begin, end) =
|
|||
return gainNode;
|
||||
};
|
||||
|
||||
export const getFilter = (type, frequency, Q) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
filter.Q.value = Q;
|
||||
return filter;
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ export * from './superdough.mjs';
|
|||
export * from './sampler.mjs';
|
||||
export * from './helpers.mjs';
|
||||
export * from './synth.mjs';
|
||||
export * from './zzfx.mjs';
|
||||
export * from './logger.mjs';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "superdough",
|
||||
"version": "0.9.5",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||
const cutGroups = [];
|
||||
|
||||
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
const {
|
||||
let {
|
||||
s,
|
||||
freq,
|
||||
unit,
|
||||
|
|
@ -208,7 +208,9 @@ 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
|
||||
|
|
@ -216,6 +218,7 @@ 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;
|
||||
|
|
@ -243,19 +246,12 @@ 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;
|
||||
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)
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = offset;
|
||||
bufferSource.loopEnd = offset + duration;
|
||||
duration = loop * duration;
|
||||
}*/
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
}
|
||||
bufferSource.start(time, offset);
|
||||
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...
|
||||
|
|
@ -266,9 +262,10 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||
out.disconnect();
|
||||
onended();
|
||||
};
|
||||
const stop = (endTime, playWholeBuffer = clip === undefined) => {
|
||||
const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => {
|
||||
let releaseTime = endTime;
|
||||
if (playWholeBuffer) {
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
releaseTime = t + (end - begin) * bufferDuration;
|
||||
}
|
||||
bufferSource.stop(releaseTime + release);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import './reverb.mjs';
|
|||
import './vowel.mjs';
|
||||
import { clamp, nanFallback } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?url';
|
||||
import { getFilter, gainNode } from './helpers.mjs';
|
||||
import { createFilter, gainNode } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
|
|
@ -121,6 +121,36 @@ function getReverb(orbit, duration = 2) {
|
|||
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);
|
||||
|
|
@ -137,6 +167,8 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
);
|
||||
}
|
||||
|
||||
// duration is passed as value too..
|
||||
value.duration = hapDuration;
|
||||
// calculate absolute time
|
||||
let t = ac.currentTime + deadline;
|
||||
// destructure
|
||||
|
|
@ -145,14 +177,32 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
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,
|
||||
|
|
@ -167,6 +217,8 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
room,
|
||||
size = 2,
|
||||
velocity = 1,
|
||||
analyze, // analyser wet
|
||||
fft = 8, // fftSize 0 - 10
|
||||
} = value;
|
||||
gain = nanFallback(gain, 1);
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
|
|
@ -206,11 +258,76 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
// 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));
|
||||
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 }));
|
||||
|
|
@ -242,12 +359,19 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
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]);
|
||||
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
|
||||
};
|
||||
|
||||
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||
import { getOscillator, gainNode, getEnvelope } from './helpers.mjs';
|
||||
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
|
||||
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
const ctx = getAudioContext();
|
||||
|
|
@ -26,37 +26,75 @@ export function registerSynthSounds() {
|
|||
wave,
|
||||
(t, value, onended) => {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
const {
|
||||
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
|
||||
n = note || n || 36;
|
||||
if (typeof n === 'string') {
|
||||
n = noteToMidi(n); // e.g. c3 => 48
|
||||
note = note || 36;
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note); // e.g. c3 => 48
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof n === 'number') {
|
||||
freq = midiToFreq(n); // + 48);
|
||||
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 });
|
||||
const { node: o, stop } = getOscillator({
|
||||
t,
|
||||
s: wave,
|
||||
freq,
|
||||
vib,
|
||||
vibmod,
|
||||
partials: n,
|
||||
});
|
||||
|
||||
let stopFm;
|
||||
// FM + FM envelope
|
||||
let stopFm, fmEnvelope;
|
||||
if (fmModulationIndex) {
|
||||
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex);
|
||||
modulator.connect(o.frequency);
|
||||
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);
|
||||
// envelope
|
||||
|
||||
// gain envelope
|
||||
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t);
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
|
|
@ -66,6 +104,7 @@ export function registerSynthSounds() {
|
|||
node: o.connect(g).connect(envelope),
|
||||
stop: (releaseTime) => {
|
||||
releaseEnvelope(releaseTime);
|
||||
fmEnvelope?.stop(releaseTime);
|
||||
let end = releaseTime + release;
|
||||
stop(end);
|
||||
stopFm?.(end);
|
||||
|
|
@ -76,3 +115,67 @@ export function registerSynthSounds() {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
124
packages/superdough/zzfx.mjs
Normal file
124
packages/superdough/zzfx.mjs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
//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
packages/superdough/zzfx_fork.mjs
Normal file
120
packages/superdough/zzfx_fork.mjs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/transpiler",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel/web",
|
||||
"version": "0.8.3",
|
||||
"version": "0.9.0",
|
||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||
"main": "web.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ 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';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.0",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
|
|
|||
88
packages/webaudio/scope.mjs
Normal file
88
packages/webaudio/scope.mjs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue