Merge pull request #684 from Bubobubobubobubo/zzfx

ZZFX Synth support
This commit is contained in:
Felix Roos 2023-08-31 13:00:30 +02:00 committed by GitHub
commit e4ea786956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 443 additions and 110 deletions

View File

@ -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("<exp lin>")
* .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) {

View File

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

View File

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

View File

@ -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';

View File

@ -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

View File

@ -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]]));
}

View File

@ -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;
}

View File

@ -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 ]",
]
`;

View File

@ -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`):
<MiniRepl client:idle tune={`note("c2 <eb2 <g2 g1>>").s('sawtooth')`} />
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.sound("<sawtooth square triangle sine>")
.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
<MiniRepl client:idle tune={`note("c2 <eb2 <g2 g1>>").s("<sawtooth square triangle>")`} />
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`.
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.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`:
<MiniRepl
client:idle
tune={`note("c2 <eb2 <g2 g1>>")
.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
<JsDoc client:idle name="fm" h={0} />
@ -54,4 +78,41 @@ Now we not only pattern the notes, but the sound as well!
<JsDoc client:idle name="fmenv" h={0} />
## 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:
<MiniRepl
client:idle
tune={`note("<c2 eb2 f2 g2>") // also supports freq
.s("<z_sawtooth z_tan z_noise z_sine z_square>")
.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)...

View File

@ -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