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/package.json b/packages/superdough/package.json index 159ea21d..d82448d6 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -36,7 +36,6 @@ "vite": "^4.3.3" }, "dependencies": { - "nanostores": "^0.8.1", - "zzfx": "^1.2.0" + "nanostores": "^0.8.1" } } diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index 352a88a2..4e6774bd 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -1,4 +1,4 @@ -import { ZZFX } from 'zzfx'; +//import { ZZFX } from 'zzfx'; import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; @@ -82,7 +82,7 @@ export const getZZFX = (value, t, duration) => { const readableParams = Object.fromEntries(paramOrder.map((param, i) => [param, params[i]])); console.log(readableParams); - const samples = ZZFX.buildSamples(...params); + const samples = /* ZZFX. */ buildSamples(...params); const context = getAudioContext(); const buffer = context.createBuffer(1, samples.length, context.sampleRate); buffer.getChannelData(0).set(samples); @@ -98,7 +98,7 @@ export function registerZZFXSounds() { console.log('registerZZFXSounds'); ['zsine', 'zsaw', 'ztri', 'ztan', 'znoise'].forEach((wave) => { registerSound(wave, (t, value, onended) => { - const duration = 0.2; + const duration = 0.3; const { node: o } = getZZFX({ s: wave, ...value }, t, duration); o.onended = () => { o.disconnect(); @@ -111,3 +111,122 @@ export function registerZZFXSounds() { }); }); } + +// https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6 +// changes: replaced this.volume with 1 + using sampleRate from getAudioContext() +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/pnpm-lock.yaml b/pnpm-lock.yaml index e328a467..25b744d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - importers: .: @@ -395,9 +391,6 @@ importers: nanostores: specifier: ^0.8.1 version: 0.8.1 - zzfx: - specifier: ^1.2.0 - version: 1.2.0 devDependencies: vite: specifier: ^4.3.3 @@ -14038,6 +14031,6 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - /zzfx@1.2.0: - resolution: {integrity: sha512-RtFz6PTMfCmxTfaCOv6GWAV4YaL/T0hltiMGkd87clybO8WLPlH6kX8sNkZGFKw9YPyu1UNsUYf/5/Vn4dondA==} - dev: false +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false