Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'white', 'pink' and 'brown' oscillators + refactor synth #713

Merged
merged 14 commits into from
Oct 3, 2023
11 changes: 10 additions & 1 deletion packages/core/controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -1153,7 +1162,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'],
Expand Down
22 changes: 22 additions & 0 deletions packages/superdough/helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 63 additions & 0 deletions packages/superdough/noise.mjs
Original file line number Diff line number Diff line change
@@ -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),
};
}
173 changes: 98 additions & 75 deletions packages/superdough/synth.mjs
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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,
});

// 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 { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;

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

Expand All @@ -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);
},
};
},
Expand Down Expand Up @@ -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);
},
};
}
4 changes: 2 additions & 2 deletions packages/superdough/zzfx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,7 +54,7 @@ export const getZZFX = (value, t) => {
pitchJump,
pitchJumpTime,
lfo,
noise,
znoise,
zmod,
zcrush,
zdelay,
Expand Down
9 changes: 9 additions & 0 deletions test/__snapshots__/examples.test.mjs.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]",
Expand Down
Loading
Loading