Merge branch 'main' into midi-in

This commit is contained in:
Felix Roos 2023-09-17 17:26:42 +02:00
commit 30cba7b935
30 changed files with 1028 additions and 255 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "0.8.4",
"version": "0.9.0",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {

View file

@ -299,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
/**
@ -323,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
@ -343,7 +370,6 @@ const generic_params = [
*
*/
['coarse'],
/**
* choose the channel the pattern is sent to in superdirt
*
@ -377,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.
*
@ -393,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.

View file

@ -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",

View file

@ -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) => ({

View file

@ -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": {

View file

@ -1,6 +1,7 @@
{
"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",

View file

@ -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": {

View file

@ -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",

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -1,4 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp } from './util.mjs';
export function gainNode(value) {
const node = getAudioContext().createGain();
@ -66,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;
}

View file

@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "0.9.6",
"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",

View file

@ -196,7 +196,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,
@ -207,7 +207,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
@ -215,6 +217,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;
@ -242,19 +245,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...
@ -265,9 +261,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);

View file

@ -9,7 +9,7 @@ import './reverb.mjs';
import './vowel.mjs';
import { clamp } 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';
@ -177,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,
@ -239,11 +257,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 }));

View file

@ -40,6 +40,8 @@ export function registerSynthSounds() {
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
@ -53,7 +55,14 @@ export function registerSynthSounds() {
}
// 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, partials: n });
const { node: o, stop } = getOscillator({
t,
s: wave,
freq,
vib,
vibmod,
partials: n,
});
// FM + FM envelope
let stopFm, fmEnvelope;
@ -137,8 +146,8 @@ export function waveformN(partials, type) {
return osc;
}
export function getOscillator({ s, freq, t, partials }) {
// make oscillator
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
// Make oscillator with partial count
let o;
if (!partials || s === 'sine') {
o = getAudioContext().createOscillator();
@ -148,7 +157,25 @@ export function getOscillator({ s, freq, t, partials }) {
}
o.frequency.value = Number(freq);
o.start(t);
//o.stop(t + duration + release);
const stop = (time) => o.stop(time);
return { node: o, stop };
// 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);
},
};
}

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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",

View file

@ -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": {