Merge remote-tracking branch 'upstream/main' into vibrato

This commit is contained in:
Felix Roos 2023-09-17 09:46:12 +02:00
commit 887fb6a84c
24 changed files with 1166 additions and 192 deletions

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.
*

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

@ -6,3 +6,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';

View 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);
});

View 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 });
});
}
});
};

View file

@ -1,7 +1,7 @@
{
"name": "@strudel/desktopbridge",
"version": "0.1.0",
"description": "send midi messages between the JS and Tauri (Rust) sides of the Studel desktop app",
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
"main": "index.mjs",
"type": "module",
"repository": {

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.7",
"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 }));