mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Merge branch 'main' into reverb
This commit is contained in:
commit
a4386a617a
@ -6,7 +6,7 @@ export let sliderValues = {};
|
||||
const getSliderID = (from) => `slider_${from}`;
|
||||
|
||||
export class SliderWidget extends WidgetType {
|
||||
constructor(value, min, max, from, to, view) {
|
||||
constructor(value, min, max, from, to, step, view) {
|
||||
super();
|
||||
this.value = value;
|
||||
this.min = min;
|
||||
@ -14,6 +14,7 @@ export class SliderWidget extends WidgetType {
|
||||
this.from = from;
|
||||
this.originalFrom = from;
|
||||
this.to = to;
|
||||
this.step = step;
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@ -29,7 +30,7 @@ export class SliderWidget extends WidgetType {
|
||||
slider.type = 'range';
|
||||
slider.min = this.min;
|
||||
slider.max = this.max;
|
||||
slider.step = (this.max - this.min) / 1000;
|
||||
slider.step = this.step ?? (this.max - this.min) / 1000;
|
||||
slider.originalValue = this.value;
|
||||
// to make sure the code stays in sync, let's save the original value
|
||||
// becuase .value automatically clamps values so it'll desync with the code
|
||||
@ -66,9 +67,9 @@ export const updateWidgets = (view, widgets) => {
|
||||
};
|
||||
|
||||
function getWidgets(widgetConfigs, view) {
|
||||
return widgetConfigs.map(({ from, to, value, min, max }) => {
|
||||
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
|
||||
return Decoration.widget({
|
||||
widget: new SliderWidget(value, min, max, from, to, view),
|
||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
||||
side: 0,
|
||||
}).range(from /* , to */);
|
||||
});
|
||||
@ -90,8 +91,10 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
||||
while (iterator.value) {
|
||||
// when the widgets are moved, we need to tell the dom node the current position
|
||||
// this is important because the updateSliderValue function has to work with the dom node
|
||||
iterator.value.widget.slider.from = iterator.from;
|
||||
iterator.value.widget.slider.to = iterator.to;
|
||||
if (iterator.value?.widget?.slider) {
|
||||
iterator.value.widget.slider.from = iterator.from;
|
||||
iterator.value.widget.slider.to = iterator.to;
|
||||
}
|
||||
iterator.next();
|
||||
}
|
||||
}
|
||||
|
||||
@ -655,6 +655,15 @@ const generic_params = [
|
||||
* .vib("<.5 1 2 4 8 16>:12")
|
||||
*/
|
||||
[['vib', 'vibmod'], 'vibrato', 'v'],
|
||||
/**
|
||||
* Adds pink noise to the mix
|
||||
*
|
||||
* @name noise
|
||||
* @param {number | Pattern} wet wet amount
|
||||
* @example
|
||||
* sound("<white pink brown>/2")
|
||||
*/
|
||||
['noise'],
|
||||
/**
|
||||
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
|
||||
*
|
||||
@ -1197,7 +1206,7 @@ const generic_params = [
|
||||
['pitchJump'],
|
||||
['pitchJumpTime'],
|
||||
['lfo', 'repeatTime'],
|
||||
['noise'],
|
||||
['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :)
|
||||
['zmod'],
|
||||
['zcrush'], // like crush but scaled differently
|
||||
['zdelay'],
|
||||
|
||||
@ -112,3 +112,25 @@ export function createFilter(
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
// stays 1 until .5, then fades out
|
||||
let wetfade = (d) => (d < 0.5 ? 1 : 1 - (d - 0.5) / 0.5);
|
||||
|
||||
// mix together dry and wet nodes. 0 = only dry 1 = only wet
|
||||
// still not too sure about how this could be used more generally...
|
||||
export function drywet(dry, wet, wetAmount = 0) {
|
||||
const ac = getAudioContext();
|
||||
if (!wetAmount) {
|
||||
return dry;
|
||||
}
|
||||
let dry_gain = ac.createGain();
|
||||
let wet_gain = ac.createGain();
|
||||
dry.connect(dry_gain);
|
||||
wet.connect(wet_gain);
|
||||
dry_gain.gain.value = wetfade(wetAmount);
|
||||
wet_gain.gain.value = wetfade(1 - wetAmount);
|
||||
let mix = ac.createGain();
|
||||
dry_gain.connect(mix);
|
||||
wet_gain.connect(mix);
|
||||
return mix;
|
||||
}
|
||||
|
||||
63
packages/superdough/noise.mjs
Normal file
63
packages/superdough/noise.mjs
Normal file
@ -0,0 +1,63 @@
|
||||
import { drywet } from './helpers.mjs';
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
|
||||
let noiseCache = {};
|
||||
|
||||
// lazy generates noise buffers and keeps them forever
|
||||
function getNoiseBuffer(type) {
|
||||
const ac = getAudioContext();
|
||||
if (noiseCache[type]) {
|
||||
return noiseCache[type];
|
||||
}
|
||||
const bufferSize = 2 * ac.sampleRate;
|
||||
const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate);
|
||||
const output = noiseBuffer.getChannelData(0);
|
||||
let lastOut = 0;
|
||||
let b0, b1, b2, b3, b4, b5, b6;
|
||||
b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0;
|
||||
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
if (type === 'white') {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
} else if (type === 'brown') {
|
||||
let white = Math.random() * 2 - 1;
|
||||
output[i] = (lastOut + 0.02 * white) / 1.02;
|
||||
lastOut = output[i];
|
||||
} else if (type === 'pink') {
|
||||
let white = Math.random() * 2 - 1;
|
||||
b0 = 0.99886 * b0 + white * 0.0555179;
|
||||
b1 = 0.99332 * b1 + white * 0.0750759;
|
||||
b2 = 0.969 * b2 + white * 0.153852;
|
||||
b3 = 0.8665 * b3 + white * 0.3104856;
|
||||
b4 = 0.55 * b4 + white * 0.5329522;
|
||||
b5 = -0.7616 * b5 - white * 0.016898;
|
||||
output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
|
||||
output[i] *= 0.11;
|
||||
b6 = white * 0.115926;
|
||||
}
|
||||
}
|
||||
noiseCache[type] = noiseBuffer;
|
||||
return noiseBuffer;
|
||||
}
|
||||
|
||||
// expects one of noises as type
|
||||
export function getNoiseOscillator(type = 'white', t) {
|
||||
const ac = getAudioContext();
|
||||
const o = ac.createBufferSource();
|
||||
o.buffer = getNoiseBuffer(type);
|
||||
o.loop = true;
|
||||
o.start(t);
|
||||
return {
|
||||
node: o,
|
||||
stop: (time) => o.stop(time),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNoiseMix(inputNode, wet, t) {
|
||||
const noiseOscillator = getNoiseOscillator('pink', t);
|
||||
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
||||
return {
|
||||
node: noiseMix,
|
||||
stop: (time) => noiseOscillator?.stop(time),
|
||||
};
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
const ctx = getAudioContext();
|
||||
@ -20,75 +21,26 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
||||
return mod(modfreq, modgain, wave);
|
||||
};
|
||||
|
||||
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
|
||||
const noises = ['pink', 'white', 'brown'];
|
||||
|
||||
export function registerSynthSounds() {
|
||||
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
|
||||
[...waveforms, ...noises].forEach((s) => {
|
||||
registerSound(
|
||||
wave,
|
||||
s,
|
||||
(t, value, onended) => {
|
||||
// destructure adsr here, because the default should be different for synths and samples
|
||||
let {
|
||||
attack = 0.001,
|
||||
decay = 0.05,
|
||||
sustain = 0.6,
|
||||
release = 0.01,
|
||||
fmh: fmHarmonicity = 1,
|
||||
fmi: fmModulationIndex,
|
||||
fmenv: fmEnvelopeType = 'lin',
|
||||
fmattack: fmAttack,
|
||||
fmdecay: fmDecay,
|
||||
fmsustain: fmSustain,
|
||||
fmrelease: fmRelease,
|
||||
fmvelocity: fmVelocity,
|
||||
fmwave: fmWaveform = 'sine',
|
||||
vib = 0,
|
||||
vibmod = 0.5,
|
||||
} = value;
|
||||
let { n, note, freq } = value;
|
||||
// with synths, n and note are the same thing
|
||||
note = note || 36;
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note); // e.g. c3 => 48
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof note === 'number') {
|
||||
freq = midiToFreq(note); // + 48);
|
||||
}
|
||||
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
|
||||
// make oscillator
|
||||
const { node: o, stop } = getOscillator({
|
||||
t,
|
||||
s: wave,
|
||||
freq,
|
||||
vib,
|
||||
vibmod,
|
||||
partials: n,
|
||||
});
|
||||
let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
|
||||
|
||||
// FM + FM envelope
|
||||
let stopFm, fmEnvelope;
|
||||
if (fmModulationIndex) {
|
||||
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
|
||||
// no envelope by default
|
||||
modulator.connect(o.frequency);
|
||||
} else {
|
||||
fmAttack = fmAttack ?? 0.001;
|
||||
fmDecay = fmDecay ?? 0.001;
|
||||
fmSustain = fmSustain ?? 1;
|
||||
fmRelease = fmRelease ?? 0.001;
|
||||
fmVelocity = fmVelocity ?? 1;
|
||||
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
|
||||
if (fmEnvelopeType === 'exp') {
|
||||
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
|
||||
fmEnvelope.node.maxValue = fmModulationIndex * 2;
|
||||
fmEnvelope.node.minValue = 0.00001;
|
||||
}
|
||||
modulator.connect(fmEnvelope.node);
|
||||
fmEnvelope.node.connect(o.frequency);
|
||||
}
|
||||
stopFm = stop;
|
||||
let sound;
|
||||
if (waveforms.includes(s)) {
|
||||
sound = getOscillator(s, t, value);
|
||||
} else {
|
||||
sound = getNoiseOscillator(s, t);
|
||||
}
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
||||
// turn down
|
||||
const g = gainNode(0.3);
|
||||
|
||||
@ -104,10 +56,9 @@ export function registerSynthSounds() {
|
||||
node: o.connect(g).connect(envelope),
|
||||
stop: (releaseTime) => {
|
||||
releaseEnvelope(releaseTime);
|
||||
fmEnvelope?.stop(releaseTime);
|
||||
triggerRelease?.(releaseTime);
|
||||
let end = releaseTime + release;
|
||||
stop(end);
|
||||
stopFm?.(end);
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -146,36 +97,108 @@ export function waveformN(partials, type) {
|
||||
return osc;
|
||||
}
|
||||
|
||||
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
|
||||
// Make oscillator with partial count
|
||||
// expects one of waveforms as s
|
||||
export function getOscillator(
|
||||
s,
|
||||
t,
|
||||
{
|
||||
n: partials,
|
||||
note,
|
||||
freq,
|
||||
vib = 0,
|
||||
vibmod = 0.5,
|
||||
noise = 0,
|
||||
// fm
|
||||
fmh: fmHarmonicity = 1,
|
||||
fmi: fmModulationIndex,
|
||||
fmenv: fmEnvelopeType = 'lin',
|
||||
fmattack: fmAttack,
|
||||
fmdecay: fmDecay,
|
||||
fmsustain: fmSustain,
|
||||
fmrelease: fmRelease,
|
||||
fmvelocity: fmVelocity,
|
||||
fmwave: fmWaveform = 'sine',
|
||||
},
|
||||
) {
|
||||
let ac = getAudioContext();
|
||||
let o;
|
||||
// If no partials are given, use stock waveforms
|
||||
if (!partials || s === 'sine') {
|
||||
o = getAudioContext().createOscillator();
|
||||
o.type = s || 'triangle';
|
||||
} else {
|
||||
}
|
||||
// generate custom waveform if partials are given
|
||||
else {
|
||||
o = waveformN(partials, s);
|
||||
}
|
||||
|
||||
// get frequency from note...
|
||||
note = note || 36;
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note); // e.g. c3 => 48
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof note === 'number') {
|
||||
freq = midiToFreq(note); // + 48);
|
||||
}
|
||||
|
||||
// set frequency
|
||||
o.frequency.value = Number(freq);
|
||||
o.start(t);
|
||||
|
||||
// FM
|
||||
let stopFm, fmEnvelope;
|
||||
if (fmModulationIndex) {
|
||||
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
|
||||
// no envelope by default
|
||||
modulator.connect(o.frequency);
|
||||
} else {
|
||||
fmAttack = fmAttack ?? 0.001;
|
||||
fmDecay = fmDecay ?? 0.001;
|
||||
fmSustain = fmSustain ?? 1;
|
||||
fmRelease = fmRelease ?? 0.001;
|
||||
fmVelocity = fmVelocity ?? 1;
|
||||
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
|
||||
if (fmEnvelopeType === 'exp') {
|
||||
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
|
||||
fmEnvelope.node.maxValue = fmModulationIndex * 2;
|
||||
fmEnvelope.node.minValue = 0.00001;
|
||||
}
|
||||
modulator.connect(fmEnvelope.node);
|
||||
fmEnvelope.node.connect(o.frequency);
|
||||
}
|
||||
stopFm = stop;
|
||||
}
|
||||
|
||||
// Additional oscillator for vibrato effect
|
||||
let vibrato_oscillator;
|
||||
let vibratoOscillator;
|
||||
if (vib > 0) {
|
||||
vibrato_oscillator = getAudioContext().createOscillator();
|
||||
vibrato_oscillator.frequency.value = vib;
|
||||
vibratoOscillator = getAudioContext().createOscillator();
|
||||
vibratoOscillator.frequency.value = vib;
|
||||
const gain = getAudioContext().createGain();
|
||||
// Vibmod is the amount of vibrato, in semitones
|
||||
gain.gain.value = vibmod * 100;
|
||||
vibrato_oscillator.connect(gain);
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(o.detune);
|
||||
vibrato_oscillator.start(t);
|
||||
vibratoOscillator.start(t);
|
||||
}
|
||||
|
||||
let noiseMix;
|
||||
if (noise) {
|
||||
noiseMix = getNoiseMix(o, noise, t);
|
||||
}
|
||||
|
||||
return {
|
||||
node: o,
|
||||
node: noiseMix?.node || o,
|
||||
stop: (time) => {
|
||||
vibrato_oscillator?.stop(time);
|
||||
vibratoOscillator?.stop(time);
|
||||
noiseMix?.stop(time);
|
||||
stopFm?.(time);
|
||||
o.stop(time);
|
||||
},
|
||||
triggerRelease: (time) => {
|
||||
fmEnvelope?.stop(time);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export const getZZFX = (value, t) => {
|
||||
pitchJump = 0,
|
||||
pitchJumpTime = 0,
|
||||
lfo = 0,
|
||||
noise = 0,
|
||||
znoise = 0,
|
||||
zmod = 0,
|
||||
zcrush = 0,
|
||||
zdelay = 0,
|
||||
@ -54,7 +54,7 @@ export const getZZFX = (value, t) => {
|
||||
pitchJump,
|
||||
pitchJumpTime,
|
||||
lfo,
|
||||
noise,
|
||||
znoise,
|
||||
zmod,
|
||||
zcrush,
|
||||
zdelay,
|
||||
|
||||
@ -43,6 +43,7 @@ export function transpiler(input, options = {}) {
|
||||
value: node.arguments[0].raw, // don't use value!
|
||||
min: node.arguments[1]?.value ?? 0,
|
||||
max: node.arguments[2]?.value ?? 1,
|
||||
step: node.arguments[3]?.value,
|
||||
});
|
||||
return this.replace(widgetWithLocation(node));
|
||||
}
|
||||
|
||||
@ -2959,6 +2959,15 @@ exports[`runs examples > example "never" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "noise" example index 0 1`] = `
|
||||
[
|
||||
"[ (0/1 → 1/1) ⇝ 2/1 | s:white ]",
|
||||
"[ 0/1 ⇜ (1/1 → 2/1) | s:white ]",
|
||||
"[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]",
|
||||
"[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "note" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
|
||||
@ -23,6 +23,25 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b
|
||||
|
||||
If you don't set a `sound` but a `note` the default value for `sound` is `triangle`!
|
||||
|
||||
## Noise
|
||||
|
||||
You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different
|
||||
flavours of noise, here written from hard to soft.
|
||||
|
||||
<MiniRepl client:idle tune={`sound("<white pink brown>/2").scope()`} />
|
||||
|
||||
Here's a more musical example of how to use noise for hihats:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`sound("bd*2,<white pink brown>*8")
|
||||
.decay(.04).sustain(0).scope()`}
|
||||
/>
|
||||
|
||||
Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter:
|
||||
|
||||
<MiniRepl client:idle tune={`note("c3").noise("<0.1 0.25 0.5>").scope()`} />
|
||||
|
||||
### Additive Synthesis
|
||||
|
||||
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user