diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index c6dbc614..71f58427 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -121,6 +121,7 @@ const generic_params = [ * note("c e g b") * .fm(4) * .fmh("<1 2 1.5 1.61>") + * .scope() * */ [['fmh', 'fmi'], 'fmh'], @@ -134,6 +135,7 @@ const generic_params = [ * @example * note("c e g b") * .fm("<0 1 2 8 32>") + * .scope() * */ [['fmi', 'fmh'], 'fm'], @@ -149,6 +151,7 @@ const generic_params = [ * .fmdecay(.2) * .fmsustain(0) * .fmenv("") + * .scope() * */ ['fmenv'], @@ -161,6 +164,7 @@ const generic_params = [ * note("c e g b") * .fm(4) * .fmattack("<0 .05 .1 .2>") + * .scope() * */ ['fmattack'], @@ -174,6 +178,7 @@ const generic_params = [ * .fm(4) * .fmdecay("<.01 .05 .1 .2>") * .fmsustain(.4) + * .scope() * */ ['fmdecay'], @@ -187,6 +192,7 @@ const generic_params = [ * .fm(4) * .fmdecay(.1) * .fmsustain("<1 .75 .5 0>") + * .scope() * */ ['fmsustain'], @@ -861,8 +867,22 @@ const generic_params = [ * */ ['clip'], -]; + // ZZFX + ['zrand'], + ['curve'], + ['slide'], // superdirt duplicate + ['deltaSlide'], + ['pitchJump'], + ['pitchJumpTime'], + ['lfo', 'repeatTime'], + ['noise'], + ['zmod'], + ['zcrush'], // like crush but scaled differently + ['zdelay'], + ['tremolo'], + ['zzfx'], +]; // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13 controls.createParam = function (names) { diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index d1cdd7be..c57baa63 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -29,6 +29,9 @@ export const getDrawContext = (id = 'test-canvas') => { }; Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { + if (typeof window === 'undefined') { + return this; + } if (window.strudelAnimation) { cancelAnimationFrame(window.strudelAnimation); } diff --git a/packages/superdough/README.md b/packages/superdough/README.md index 4d670ac6..ac46f69e 100644 --- a/packages/superdough/README.md +++ b/packages/superdough/README.md @@ -161,5 +161,6 @@ Then just make sure your first call of `superdough` happens after a click of som ## Credits +- [ZZFX](https://github.com/KilledByAPixel/ZzFX) used for synths starting with z - [SuperDirt](https://github.com/musikinformatik/SuperDirt) - [WebDirt](https://github.com/dktr0/WebDirt) diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index b795539a..e5d4498b 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -8,4 +8,5 @@ export * from './superdough.mjs'; export * from './sampler.mjs'; export * from './helpers.mjs'; export * from './synth.mjs'; +export * from './zzfx.mjs'; export * from './logger.mjs'; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index d5a2ca3f..af22355f 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -167,6 +167,8 @@ export const superdough = async (value, deadline, hapDuration) => { ); } + // duration is passed as value too.. + value.duration = hapDuration; // calculate absolute time let t = ac.currentTime + deadline; // destructure diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs new file mode 100644 index 00000000..da505d74 --- /dev/null +++ b/packages/superdough/zzfx.mjs @@ -0,0 +1,124 @@ +//import { ZZFX } from 'zzfx'; +import { midiToFreq, noteToMidi } from './util.mjs'; +import { registerSound, getAudioContext } from './superdough.mjs'; +import { buildSamples } from './zzfx_fork.mjs'; + +export const getZZFX = (value, t) => { + let { + s, + note = 36, + freq, + // + zrand = 0, + attack = 0, + decay = 0, + sustain = 0.8, + release = 0.1, + curve = 1, + slide = 0, + deltaSlide = 0, + pitchJump = 0, + pitchJumpTime = 0, + lfo = 0, + noise = 0, + zmod = 0, + zcrush = 0, + zdelay = 0, + tremolo = 0, + duration = 0.2, + zzfx, + } = value; + const sustainTime = Math.max(duration - attack - decay, 0); + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); + } + s = s.replace('z_', ''); + const shape = ['sine', 'triangle', 'sawtooth', 'tan', 'noise'].indexOf(s) || 0; + curve = s === 'square' ? 0 : curve; + + const params = zzfx || [ + 0.25, // volume + zrand, + freq, + attack, + sustainTime, + release, + shape, + curve, + slide, + deltaSlide, + pitchJump, + pitchJumpTime, + lfo, + noise, + zmod, + zcrush, + zdelay, + sustain, // sustain volume! + decay, + tremolo, + ]; + // console.log(redableZZFX(params)); + + const samples = /* ZZFX. */ buildSamples(...params); + const context = getAudioContext(); + const buffer = context.createBuffer(1, samples.length, context.sampleRate); + buffer.getChannelData(0).set(samples); + const source = getAudioContext().createBufferSource(); + source.buffer = buffer; + source.start(t); + return { + node: source, + }; +}; + +export function registerZZFXSounds() { + ['zzfx', 'z_sine', 'z_sawtooth', 'z_triangle', 'z_square', 'z_tan', 'z_noise'].forEach((wave) => { + registerSound( + wave, + (t, value, onended) => { + const { node: o } = getZZFX({ s: wave, ...value }, t); + o.onended = () => { + o.disconnect(); + onended(); + }; + return { + node: o, + stop: () => {}, + }; + }, + { type: 'synth', prebake: true }, + ); + }); +} + +// just for debugging +function redableZZFX(params) { + const paramOrder = [ + 'volume', + 'zrand', + 'frequency', + 'attack', + 'sustain', + 'release', + 'shape', + 'curve', + 'slide', + 'deltaSlide', + 'pitchJump', + 'pitchJumpTime', + 'lfo', + 'noise', + 'zmod', + 'zcrush', + 'zdelay', + 'sustainVolume', + 'decay', + 'tremolo', + ]; + return Object.fromEntries(paramOrder.map((param, i) => [param, params[i]])); +} diff --git a/packages/superdough/zzfx_fork.mjs b/packages/superdough/zzfx_fork.mjs new file mode 100644 index 00000000..8c6bdbb5 --- /dev/null +++ b/packages/superdough/zzfx_fork.mjs @@ -0,0 +1,120 @@ +import { getAudioContext } from './superdough.mjs'; + +// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6 +// changes: replaced this.volume with 1 + using sampleRate from getAudioContext() +export function buildSamples( + volume = 1, + randomness = 0.05, + frequency = 220, + attack = 0, + sustain = 0, + release = 0.1, + shape = 0, + shapeCurve = 1, + slide = 0, + deltaSlide = 0, + pitchJump = 0, + pitchJumpTime = 0, + repeatTime = 0, + noise = 0, + modulation = 0, + bitCrush = 0, + delay = 0, + sustainVolume = 1, + decay = 0, + tremolo = 0, +) { + // init parameters + let PI2 = Math.PI * 2, + sampleRate = getAudioContext().sampleRate, + sign = (v) => (v > 0 ? 1 : -1), + startSlide = (slide *= (500 * PI2) / sampleRate / sampleRate), + startFrequency = (frequency *= ((1 + randomness * 2 * Math.random() - randomness) * PI2) / sampleRate), + b = [], + t = 0, + tm = 0, + i = 0, + j = 1, + r = 0, + c = 0, + s = 0, + f, + length; + + // scale by sample rate + attack = attack * sampleRate + 9; // minimum attack to prevent pop + decay *= sampleRate; + sustain *= sampleRate; + release *= sampleRate; + delay *= sampleRate; + deltaSlide *= (500 * PI2) / sampleRate ** 3; + modulation *= PI2 / sampleRate; + pitchJump *= PI2 / sampleRate; + pitchJumpTime *= sampleRate; + repeatTime = (repeatTime * sampleRate) | 0; + + // generate waveform + for (length = (attack + decay + sustain + release + delay) | 0; i < length; b[i++] = s) { + if (!(++c % ((bitCrush * 100) | 0))) { + // bit crush + s = shape + ? shape > 1 + ? shape > 2 + ? shape > 3 // wave shape + ? Math.sin((t % PI2) ** 3) // 4 noise + : Math.max(Math.min(Math.tan(t), 1), -1) // 3 tan + : 1 - (((((2 * t) / PI2) % 2) + 2) % 2) // 2 saw + : 1 - 4 * Math.abs(Math.round(t / PI2) - t / PI2) // 1 triangle + : Math.sin(t); // 0 sin + + s = + (repeatTime + ? 1 - tremolo + tremolo * Math.sin((PI2 * i) / repeatTime) // tremolo + : 1) * + sign(s) * + Math.abs(s) ** shapeCurve * // curve 0=square, 2=pointy + volume * + 1 * // envelope + (i < attack + ? i / attack // attack + : i < attack + decay // decay + ? 1 - ((i - attack) / decay) * (1 - sustainVolume) // decay falloff + : i < attack + decay + sustain // sustain + ? sustainVolume // sustain volume + : i < length - delay // release + ? ((length - i - delay) / release) * // release falloff + sustainVolume // release volume + : 0); // post release + + s = delay + ? s / 2 + + (delay > i + ? 0 // delay + : ((i < length - delay ? 1 : (length - i) / delay) * // release delay + b[(i - delay) | 0]) / + 2) + : s; // sample delay + } + + f = + (frequency += slide += deltaSlide) * // frequency + Math.cos(modulation * tm++); // modulation + t += f - f * noise * (1 - (((Math.sin(i) + 1) * 1e9) % 2)); // noise + + if (j && ++j > pitchJumpTime) { + // pitch jump + frequency += pitchJump; // apply pitch jump + startFrequency += pitchJump; // also apply to start + j = 0; // stop pitch jump time + } + + if (repeatTime && !(++r % repeatTime)) { + // repeat + frequency = startFrequency; // reset frequency + slide = startSlide; // reset slide + j ||= 1; // reset pitch jump time + } + } + + return b; +} diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 787bc461..60b39c53 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1806,127 +1806,127 @@ exports[`runs examples > example "floor" example index 0 1`] = ` exports[`runs examples > example "fm" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c fmi:0 ]", - "[ 1/4 → 1/2 | note:e fmi:0 ]", - "[ 1/2 → 3/4 | note:g fmi:0 ]", - "[ 3/4 → 1/1 | note:b fmi:0 ]", - "[ 1/1 → 5/4 | note:c fmi:1 ]", - "[ 5/4 → 3/2 | note:e fmi:1 ]", - "[ 3/2 → 7/4 | note:g fmi:1 ]", - "[ 7/4 → 2/1 | note:b fmi:1 ]", - "[ 2/1 → 9/4 | note:c fmi:2 ]", - "[ 9/4 → 5/2 | note:e fmi:2 ]", - "[ 5/2 → 11/4 | note:g fmi:2 ]", - "[ 11/4 → 3/1 | note:b fmi:2 ]", - "[ 3/1 → 13/4 | note:c fmi:8 ]", - "[ 13/4 → 7/2 | note:e fmi:8 ]", - "[ 7/2 → 15/4 | note:g fmi:8 ]", - "[ 15/4 → 4/1 | note:b fmi:8 ]", + "[ 0/1 → 1/4 | note:c fmi:0 analyze:1 ]", + "[ 1/4 → 1/2 | note:e fmi:0 analyze:1 ]", + "[ 1/2 → 3/4 | note:g fmi:0 analyze:1 ]", + "[ 3/4 → 1/1 | note:b fmi:0 analyze:1 ]", + "[ 1/1 → 5/4 | note:c fmi:1 analyze:1 ]", + "[ 5/4 → 3/2 | note:e fmi:1 analyze:1 ]", + "[ 3/2 → 7/4 | note:g fmi:1 analyze:1 ]", + "[ 7/4 → 2/1 | note:b fmi:1 analyze:1 ]", + "[ 2/1 → 9/4 | note:c fmi:2 analyze:1 ]", + "[ 9/4 → 5/2 | note:e fmi:2 analyze:1 ]", + "[ 5/2 → 11/4 | note:g fmi:2 analyze:1 ]", + "[ 11/4 → 3/1 | note:b fmi:2 analyze:1 ]", + "[ 3/1 → 13/4 | note:c fmi:8 analyze:1 ]", + "[ 13/4 → 7/2 | note:e fmi:8 analyze:1 ]", + "[ 7/2 → 15/4 | note:g fmi:8 analyze:1 ]", + "[ 15/4 → 4/1 | note:b fmi:8 analyze:1 ]", ] `; exports[`runs examples > example "fmattack" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c fmi:4 fmattack:0 ]", - "[ 1/4 → 1/2 | note:e fmi:4 fmattack:0 ]", - "[ 1/2 → 3/4 | note:g fmi:4 fmattack:0 ]", - "[ 3/4 → 1/1 | note:b fmi:4 fmattack:0 ]", - "[ 1/1 → 5/4 | note:c fmi:4 fmattack:0.05 ]", - "[ 5/4 → 3/2 | note:e fmi:4 fmattack:0.05 ]", - "[ 3/2 → 7/4 | note:g fmi:4 fmattack:0.05 ]", - "[ 7/4 → 2/1 | note:b fmi:4 fmattack:0.05 ]", - "[ 2/1 → 9/4 | note:c fmi:4 fmattack:0.1 ]", - "[ 9/4 → 5/2 | note:e fmi:4 fmattack:0.1 ]", - "[ 5/2 → 11/4 | note:g fmi:4 fmattack:0.1 ]", - "[ 11/4 → 3/1 | note:b fmi:4 fmattack:0.1 ]", - "[ 3/1 → 13/4 | note:c fmi:4 fmattack:0.2 ]", - "[ 13/4 → 7/2 | note:e fmi:4 fmattack:0.2 ]", - "[ 7/2 → 15/4 | note:g fmi:4 fmattack:0.2 ]", - "[ 15/4 → 4/1 | note:b fmi:4 fmattack:0.2 ]", + "[ 0/1 → 1/4 | note:c fmi:4 fmattack:0 analyze:1 ]", + "[ 1/4 → 1/2 | note:e fmi:4 fmattack:0 analyze:1 ]", + "[ 1/2 → 3/4 | note:g fmi:4 fmattack:0 analyze:1 ]", + "[ 3/4 → 1/1 | note:b fmi:4 fmattack:0 analyze:1 ]", + "[ 1/1 → 5/4 | note:c fmi:4 fmattack:0.05 analyze:1 ]", + "[ 5/4 → 3/2 | note:e fmi:4 fmattack:0.05 analyze:1 ]", + "[ 3/2 → 7/4 | note:g fmi:4 fmattack:0.05 analyze:1 ]", + "[ 7/4 → 2/1 | note:b fmi:4 fmattack:0.05 analyze:1 ]", + "[ 2/1 → 9/4 | note:c fmi:4 fmattack:0.1 analyze:1 ]", + "[ 9/4 → 5/2 | note:e fmi:4 fmattack:0.1 analyze:1 ]", + "[ 5/2 → 11/4 | note:g fmi:4 fmattack:0.1 analyze:1 ]", + "[ 11/4 → 3/1 | note:b fmi:4 fmattack:0.1 analyze:1 ]", + "[ 3/1 → 13/4 | note:c fmi:4 fmattack:0.2 analyze:1 ]", + "[ 13/4 → 7/2 | note:e fmi:4 fmattack:0.2 analyze:1 ]", + "[ 7/2 → 15/4 | note:g fmi:4 fmattack:0.2 analyze:1 ]", + "[ 15/4 → 4/1 | note:b fmi:4 fmattack:0.2 analyze:1 ]", ] `; exports[`runs examples > example "fmdecay" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 ]", - "[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 ]", - "[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 ]", - "[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 ]", - "[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 ]", - "[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 ]", - "[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 ]", - "[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 ]", - "[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 ]", - "[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 ]", - "[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 ]", - "[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 ]", - "[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 ]", - "[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 ]", - "[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 ]", - "[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 ]", + "[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", + "[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", + "[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", + "[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", + "[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", + "[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", + "[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", + "[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", + "[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", + "[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", + "[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", + "[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", + "[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", + "[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", + "[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", + "[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", ] `; exports[`runs examples > example "fmenv" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", - "[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", - "[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", - "[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", - "[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]", - "[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", - "[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", - "[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", - "[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]", + "[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", + "[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", + "[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", ] `; exports[`runs examples > example "fmh" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c fmi:4 fmh:1 ]", - "[ 1/4 → 1/2 | note:e fmi:4 fmh:1 ]", - "[ 1/2 → 3/4 | note:g fmi:4 fmh:1 ]", - "[ 3/4 → 1/1 | note:b fmi:4 fmh:1 ]", - "[ 1/1 → 5/4 | note:c fmi:4 fmh:2 ]", - "[ 5/4 → 3/2 | note:e fmi:4 fmh:2 ]", - "[ 3/2 → 7/4 | note:g fmi:4 fmh:2 ]", - "[ 7/4 → 2/1 | note:b fmi:4 fmh:2 ]", - "[ 2/1 → 9/4 | note:c fmi:4 fmh:1.5 ]", - "[ 9/4 → 5/2 | note:e fmi:4 fmh:1.5 ]", - "[ 5/2 → 11/4 | note:g fmi:4 fmh:1.5 ]", - "[ 11/4 → 3/1 | note:b fmi:4 fmh:1.5 ]", - "[ 3/1 → 13/4 | note:c fmi:4 fmh:1.61 ]", - "[ 13/4 → 7/2 | note:e fmi:4 fmh:1.61 ]", - "[ 7/2 → 15/4 | note:g fmi:4 fmh:1.61 ]", - "[ 15/4 → 4/1 | note:b fmi:4 fmh:1.61 ]", + "[ 0/1 → 1/4 | note:c fmi:4 fmh:1 analyze:1 ]", + "[ 1/4 → 1/2 | note:e fmi:4 fmh:1 analyze:1 ]", + "[ 1/2 → 3/4 | note:g fmi:4 fmh:1 analyze:1 ]", + "[ 3/4 → 1/1 | note:b fmi:4 fmh:1 analyze:1 ]", + "[ 1/1 → 5/4 | note:c fmi:4 fmh:2 analyze:1 ]", + "[ 5/4 → 3/2 | note:e fmi:4 fmh:2 analyze:1 ]", + "[ 3/2 → 7/4 | note:g fmi:4 fmh:2 analyze:1 ]", + "[ 7/4 → 2/1 | note:b fmi:4 fmh:2 analyze:1 ]", + "[ 2/1 → 9/4 | note:c fmi:4 fmh:1.5 analyze:1 ]", + "[ 9/4 → 5/2 | note:e fmi:4 fmh:1.5 analyze:1 ]", + "[ 5/2 → 11/4 | note:g fmi:4 fmh:1.5 analyze:1 ]", + "[ 11/4 → 3/1 | note:b fmi:4 fmh:1.5 analyze:1 ]", + "[ 3/1 → 13/4 | note:c fmi:4 fmh:1.61 analyze:1 ]", + "[ 13/4 → 7/2 | note:e fmi:4 fmh:1.61 analyze:1 ]", + "[ 7/2 → 15/4 | note:g fmi:4 fmh:1.61 analyze:1 ]", + "[ 15/4 → 4/1 | note:b fmi:4 fmh:1.61 analyze:1 ]", ] `; exports[`runs examples > example "fmsustain" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]", - "[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.1 fmsustain:1 ]", - "[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.1 fmsustain:1 ]", - "[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.1 fmsustain:1 ]", - "[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 ]", - "[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 ]", - "[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 ]", - "[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 ]", - "[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 ]", - "[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 ]", - "[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 ]", - "[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 ]", - "[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0 ]", - "[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0 ]", - "[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0 ]", - "[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0 ]", + "[ 0/1 → 1/4 | note:c fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", + "[ 1/4 → 1/2 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", + "[ 1/2 → 3/4 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", + "[ 3/4 → 1/1 | note:b fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", + "[ 1/1 → 5/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", + "[ 5/4 → 3/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", + "[ 3/2 → 7/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", + "[ 7/4 → 2/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", + "[ 2/1 → 9/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", + "[ 9/4 → 5/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", + "[ 5/2 → 11/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", + "[ 11/4 → 3/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", + "[ 3/1 → 13/4 | note:c fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", + "[ 13/4 → 7/2 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", + "[ 7/2 → 15/4 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", + "[ 15/4 → 4/1 | note:b fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", ] `; diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index 9c515752..b24726cf 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -8,28 +8,52 @@ import { JsDoc } from '../../docs/JsDoc'; # Synths -For now, [samples](/learn/samples) are the main way to play with Strudel. -In the future, more powerful synthesis capabilities will be added. -If in the meantime you want to dive deeper into audio synthesis with Tidal, you will need to [install SuperCollider and SuperDirt](https://tidalcycles.org/docs/). +In addition to the sampling engine, strudel comes with a synthesizer to create sounds on the fly. -## Playing synths with `s` +## Basic Waveforms -We can change the sound, using the `s` function: +The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can be selected via `sound` (or `s`): ->").s('sawtooth')`} /> +>") +.sound("") +.scope()`} +/> -Here, we are wrapping our notes inside `note` and set the sound using `s`, connected by a dot. +If you don't set a `sound` but a `note` the default value for `sound` is `triangle`! -Those functions are only 2 of many ways to alter the properties, or _params_ of a sound. -The power of patterns allows us to sequence any _param_ independently: +### Additive Synthesis ->").s("")`} /> +To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform: -Now we not only pattern the notes, but the sound as well! -`sawtooth` `square` and `triangle` are the basic waveforms available in `s`. +>") +.sound("sawtooth") +.n("<32 16 8 4>") +.scope()`} +/> + +When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. +You can also set `n` directly in mini notation with `sound`: + +>") +.sound("sawtooth:<32 16 8 4>") +.scope()`} +/> + +Note for tidal users: `n` in tidal is synonymous to `note` for synths only. +In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms. ## FM Synthesis +FM Synthesis is a technique that changes the frequency of a basic waveform rapidly to alter the timbre. + +You can use fm with any of the above waveforms, although the below examples all use the default triangle wave. + ### fm @@ -54,4 +78,41 @@ Now we not only pattern the notes, but the sound as well! +## ZZFX + +The "Zuper Zmall Zound Zynth" [ZZFX](https://github.com/KilledByAPixel/ZzFX) is also integrated in strudel. +Developed by [Frank Force](https://frankforce.com/), it is a synth and FX engine originally intended to be used for size coding games. + +It has 20 parameters in total, here is a snippet that uses all: + +") // also supports freq + .s("") + .zrand(0) // randomization + // zzfx envelope + .attack(0.001) + .decay(0.1) + .sustain(.8) + .release(.1) + // special zzfx params + .curve(1) // waveshape 1-3 + .slide(0) // +/- pitch slide + .deltaSlide(0) // +/- pitch slide (?) + .noise(0) // make it dirty + .zmod(0) // fm speed + .zcrush(0) // bit crush 0 - 1 + .zdelay(0) // simple delay + .pitchJump(0) // +/- pitch change after pitchJumpTime + .pitchJumpTime(0) // >0 time after pitchJump is applied + .lfo(0) // >0 resets slide + pitchJump + sets tremolo speed + .tremolo(0) // 0-1 lfo volume modulation amount + //.duration(.2) // overwrite strudel event duration + //.gain(1) // change volume + .scope() // vizualise waveform (not zzfx related) +`} +/> + +Note that you can also combine zzfx with all the other audio fx (next chapter). + Next up: [Audio Effects](/learn/effects)... diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 6bdc5c0d..28678c9d 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -1,5 +1,5 @@ import { Pattern, noteToMidi, valueToMidi } from '@strudel.cycles/core'; -import { registerSynthSounds, samples } from '@strudel.cycles/webaudio'; +import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycles/webaudio'; import './piano.mjs'; import './files.mjs'; @@ -8,6 +8,7 @@ export async function prebake() { // License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm await Promise.all([ registerSynthSounds(), + registerZZFXSounds(), //registerSoundfonts(), // need dynamic import here, because importing @strudel.cycles/soundfonts fails on server: // => getting "window is not defined", as soon as "@strudel.cycles/soundfonts" is imported statically