mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-21 10:38:37 +00:00
Merge pull request #713 from Bubobubobubobubo/noise
Add 'white', 'pink' and 'brown' oscillators + refactor synth
This commit is contained in:
commit
d1ffdd57a5
@ -655,6 +655,15 @@ const generic_params = [
|
|||||||
* .vib("<.5 1 2 4 8 16>:12")
|
* .vib("<.5 1 2 4 8 16>:12")
|
||||||
*/
|
*/
|
||||||
[['vib', 'vibmod'], 'vibrato', 'v'],
|
[['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
|
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
|
||||||
*
|
*
|
||||||
@ -1153,7 +1162,7 @@ const generic_params = [
|
|||||||
['pitchJump'],
|
['pitchJump'],
|
||||||
['pitchJumpTime'],
|
['pitchJumpTime'],
|
||||||
['lfo', 'repeatTime'],
|
['lfo', 'repeatTime'],
|
||||||
['noise'],
|
['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :)
|
||||||
['zmod'],
|
['zmod'],
|
||||||
['zcrush'], // like crush but scaled differently
|
['zcrush'], // like crush but scaled differently
|
||||||
['zdelay'],
|
['zdelay'],
|
||||||
|
|||||||
@ -112,3 +112,25 @@ export function createFilter(
|
|||||||
|
|
||||||
return filter;
|
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 { midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||||
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
|
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
|
||||||
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
|
||||||
const mod = (freq, range = 1, type = 'sine') => {
|
const mod = (freq, range = 1, type = 'sine') => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
@ -20,75 +21,26 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
|||||||
return mod(modfreq, modgain, wave);
|
return mod(modfreq, modgain, wave);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
|
||||||
|
const noises = ['pink', 'white', 'brown'];
|
||||||
|
|
||||||
export function registerSynthSounds() {
|
export function registerSynthSounds() {
|
||||||
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
|
[...waveforms, ...noises].forEach((s) => {
|
||||||
registerSound(
|
registerSound(
|
||||||
wave,
|
s,
|
||||||
(t, value, onended) => {
|
(t, value, onended) => {
|
||||||
// destructure adsr here, because the default should be different for synths and samples
|
// destructure adsr here, because the default should be different for synths and samples
|
||||||
let {
|
let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
|
||||||
attack = 0.001,
|
|
||||||
decay = 0.05,
|
|
||||||
sustain = 0.6,
|
|
||||||
release = 0.01,
|
|
||||||
fmh: fmHarmonicity = 1,
|
|
||||||
fmi: fmModulationIndex,
|
|
||||||
fmenv: fmEnvelopeType = 'lin',
|
|
||||||
fmattack: fmAttack,
|
|
||||||
fmdecay: fmDecay,
|
|
||||||
fmsustain: fmSustain,
|
|
||||||
fmrelease: fmRelease,
|
|
||||||
fmvelocity: fmVelocity,
|
|
||||||
fmwave: fmWaveform = 'sine',
|
|
||||||
vib = 0,
|
|
||||||
vibmod = 0.5,
|
|
||||||
} = value;
|
|
||||||
let { n, note, freq } = value;
|
|
||||||
// with synths, n and note are the same thing
|
|
||||||
note = note || 36;
|
|
||||||
if (typeof note === 'string') {
|
|
||||||
note = noteToMidi(note); // e.g. c3 => 48
|
|
||||||
}
|
|
||||||
// get frequency
|
|
||||||
if (!freq && typeof note === 'number') {
|
|
||||||
freq = midiToFreq(note); // + 48);
|
|
||||||
}
|
|
||||||
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
|
|
||||||
// make oscillator
|
|
||||||
const { node: o, stop } = getOscillator({
|
|
||||||
t,
|
|
||||||
s: wave,
|
|
||||||
freq,
|
|
||||||
vib,
|
|
||||||
vibmod,
|
|
||||||
partials: n,
|
|
||||||
});
|
|
||||||
|
|
||||||
// FM + FM envelope
|
let sound;
|
||||||
let stopFm, fmEnvelope;
|
if (waveforms.includes(s)) {
|
||||||
if (fmModulationIndex) {
|
sound = getOscillator(s, t, value);
|
||||||
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
|
} else {
|
||||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
|
sound = getNoiseOscillator(s, t);
|
||||||
// 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 { node: o, stop, triggerRelease } = sound;
|
||||||
|
|
||||||
// turn down
|
// turn down
|
||||||
const g = gainNode(0.3);
|
const g = gainNode(0.3);
|
||||||
|
|
||||||
@ -104,10 +56,9 @@ export function registerSynthSounds() {
|
|||||||
node: o.connect(g).connect(envelope),
|
node: o.connect(g).connect(envelope),
|
||||||
stop: (releaseTime) => {
|
stop: (releaseTime) => {
|
||||||
releaseEnvelope(releaseTime);
|
releaseEnvelope(releaseTime);
|
||||||
fmEnvelope?.stop(releaseTime);
|
triggerRelease?.(releaseTime);
|
||||||
let end = releaseTime + release;
|
let end = releaseTime + release;
|
||||||
stop(end);
|
stop(end);
|
||||||
stopFm?.(end);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -146,36 +97,108 @@ export function waveformN(partials, type) {
|
|||||||
return osc;
|
return osc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
|
// expects one of waveforms as s
|
||||||
// Make oscillator with partial count
|
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;
|
let o;
|
||||||
|
// If no partials are given, use stock waveforms
|
||||||
if (!partials || s === 'sine') {
|
if (!partials || s === 'sine') {
|
||||||
o = getAudioContext().createOscillator();
|
o = getAudioContext().createOscillator();
|
||||||
o.type = s || 'triangle';
|
o.type = s || 'triangle';
|
||||||
} else {
|
}
|
||||||
|
// generate custom waveform if partials are given
|
||||||
|
else {
|
||||||
o = waveformN(partials, s);
|
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.frequency.value = Number(freq);
|
||||||
o.start(t);
|
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
|
// Additional oscillator for vibrato effect
|
||||||
let vibrato_oscillator;
|
let vibratoOscillator;
|
||||||
if (vib > 0) {
|
if (vib > 0) {
|
||||||
vibrato_oscillator = getAudioContext().createOscillator();
|
vibratoOscillator = getAudioContext().createOscillator();
|
||||||
vibrato_oscillator.frequency.value = vib;
|
vibratoOscillator.frequency.value = vib;
|
||||||
const gain = getAudioContext().createGain();
|
const gain = getAudioContext().createGain();
|
||||||
// Vibmod is the amount of vibrato, in semitones
|
// Vibmod is the amount of vibrato, in semitones
|
||||||
gain.gain.value = vibmod * 100;
|
gain.gain.value = vibmod * 100;
|
||||||
vibrato_oscillator.connect(gain);
|
vibratoOscillator.connect(gain);
|
||||||
gain.connect(o.detune);
|
gain.connect(o.detune);
|
||||||
vibrato_oscillator.start(t);
|
vibratoOscillator.start(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
let noiseMix;
|
||||||
|
if (noise) {
|
||||||
|
noiseMix = getNoiseMix(o, noise, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: o,
|
node: noiseMix?.node || o,
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
vibrato_oscillator?.stop(time);
|
vibratoOscillator?.stop(time);
|
||||||
|
noiseMix?.stop(time);
|
||||||
|
stopFm?.(time);
|
||||||
o.stop(time);
|
o.stop(time);
|
||||||
},
|
},
|
||||||
|
triggerRelease: (time) => {
|
||||||
|
fmEnvelope?.stop(time);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export const getZZFX = (value, t) => {
|
|||||||
pitchJump = 0,
|
pitchJump = 0,
|
||||||
pitchJumpTime = 0,
|
pitchJumpTime = 0,
|
||||||
lfo = 0,
|
lfo = 0,
|
||||||
noise = 0,
|
znoise = 0,
|
||||||
zmod = 0,
|
zmod = 0,
|
||||||
zcrush = 0,
|
zcrush = 0,
|
||||||
zdelay = 0,
|
zdelay = 0,
|
||||||
@ -54,7 +54,7 @@ export const getZZFX = (value, t) => {
|
|||||||
pitchJump,
|
pitchJump,
|
||||||
pitchJumpTime,
|
pitchJumpTime,
|
||||||
lfo,
|
lfo,
|
||||||
noise,
|
znoise,
|
||||||
zmod,
|
zmod,
|
||||||
zcrush,
|
zcrush,
|
||||||
zdelay,
|
zdelay,
|
||||||
|
|||||||
@ -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`] = `
|
exports[`runs examples > example "note" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c ]",
|
"[ 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`!
|
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
|
### Additive Synthesis
|
||||||
|
|
||||||
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
|
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