Skip to content

Commit

Permalink
feat: wavetable oscillator (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
danigb authored Jul 4, 2024
1 parent 9cd7d4f commit d35ec18
Show file tree
Hide file tree
Showing 20 changed files with 764 additions and 120 deletions.
139 changes: 26 additions & 113 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,48 @@

[![npm version](https://img.shields.io/npm/v/synthlet)](https://www.npmjs.com/package/synthlet)

Collection of synth modules implemented as AudioWorklets written in Typescript. Currently, dsp is mostly a port of Will Pirkle's Designing Synth Plugins 2nd Edition book. Thanks Will 🙌
Collection of synth modules implemented as AudioWorklets.

```ts
import { loadSynthletNodes } from "synthlet";
import {
registerSynthletOnce,
createAdsr,
createWavetableOscillator,
} from "synthlet";

const { osc, filter, chain, adsr, now, trigger, param } =
loadSynthletNodes(context);
const audioContext = new AudioContext();
await registerSynthletOnce(audioContext);

// A WebAudio normal oscillator
// Simplest synth: Oscillator -> Filter -> Amplifier
const osc = createWavetableOscillator(audioContext);
const filter = createStateVariableFilter(audioContext);
const vca = createVca(audioContext);

const gate = trigger();
const frequency = param(440);
osc.connect(filter).connect(vca).connect(audioContext.destination);

const disconnect = chain(
mix(
osc({ type: AvOscillator.SAW, frequency, detune: 2 }),
osc({ type: AvOscillator.SAW, frequency, detune: -2 })
),
filter({ frequency: 3000 }),
adsr({ gate }),
context.destination
);
// Start sound
vca.gateOn();

gate.noteOn(now());
frequency.setValueAtTime(880, now() + 0.5);
gate.noteOff(now() + 1);

disconnect();
// Stop sound
vca.gateOff();
```

⚠️ This is extremely alpha software. Use at your own risk (and be careful with volume and filter resonance)

## FAQ (not F, not even A)

#### Why?

Basically, to learn and for others to learn from. Most open source synths are written in C or some other low level language. This library is written in Typescript to make it more accessible (at the cost of performance).

#### How is different from WebAudio API (WAA) built-in nodes?

It have more nodes, basically. Most of them are different. Some of dsp could be done by creating WAA nodes. See why?

#### How is different from Tone.js

First of all, this is alpha. If you need to do something serious, use Tone.js

Then, Tone.js and Synthlet have different scope, focus and philosophy. The scope of Synthlet is creating synths. No more. The focus is on dsp code. And the philosophy is not wrap WAA but provide more nodes.

On the other hand, Tone.js provides most of the things you need to create music with WAA.

#### References
## Install

- [Designing Synth Plugins 2nd Edition](http://www.willpirkle.com/)
- [BasicSynth: Creating a Music Synthesizer in Software](https://basicsynth.com/index.php?page=book)
- [Paul Bachelor's DSP Algorithms](https://paulbatchelor.github.io/sndkit/algos/)

## Setup

#### Package

Install using npm or any other package manager:
Install `synthlet` to install all modules:

```bash
npm i synthlet
```

#### Browser

## Usage

#### Load worklets
Or each module individually:

The first step is to load the worklets into an AudioContext. The simplest way is to load them all:

```js
import { loadSynthletNodes } from "synthlet";

const Synthlet = await loadSynthletNodes(new AudioContext());

const osc = Synthlet.osc({ frequency: 440 }).connect(destination);

// after some time...
osc.disconnect();
```

But you can choose which ones to load if you don't need or want the full library:

```js
import { loadKarplusStrongOscillatorProcessor } from "synthlet";

const ks = await loadKarplusStrongOscillatorProcessor(context);

const osc = ks({ note: 60 }).connect(context.destination);
osc.disconnect();
```

#### Create nodes

The load function returns a promise to a function that create audio nodes:

```js
import { loadLfoProcessor } from "synthlet";

const createLfo = await loadLfoProcessor(context);
const lfo = createLfo({ frequency: 10 }); // lfo is an AudioNode
```

#### Connect nodes

Each node is a normal WebAudio API `AudioNode` so the same principles apply:

```js
const osc = new OscillatorNode(context, { frequency: 440 });
lfo.connect(osc.frequency);
osc.start();
```bash
npm i @synthlet/adsr
```

## Modules

#### ADSR

An attack-decay-sustain-release envelope.

#### Lfo

A low frequency oscillator with several waveforms and extras.

#### Virtual Analog Oscillator

Oscillator based on Blip algorithm
## Documentation

#### Wavetable Oscillator
- [ADSR](/packages/adsr/README.md)
- [Noise](/packages/noise/README.md)
- [WavetableOscillator](/packages/wavetable-oscilllator/README.md)
31 changes: 28 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"name": "synthlet-workspace",
"private": true,
"workspaces": [
"packages/*"
],
Expand Down
2 changes: 1 addition & 1 deletion packages/adsr/src/processor.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const PROCESSOR = `"use strict";(()=>{function $(){let u=!1;return a=>{if(u===!1&&a>=.9)return u=!0,!0;if(u===!0&&a<.1)return u=!1,!1}}function F(u){let a=Math.exp(-1.5),c=Math.exp(-4.95),i=0,n=0,l=0,f=0,y=0,h=1,g={b:0,c:0},d={b:0,c:0},p={b:0,c:0},P=$(),b=0,t=0;return x(.01,.1,.5,.3),{agen:k,amod:M};function M(e,r,o){k(r,o);for(let s=0;s<r.length;s++)r[s]*=e[s]}function k(e,r){D(r);for(let o=0;o<e.length;o++){switch(b){case 1:t=g.b+t*g.c,(t>=1||i<=0)&&(t=1,b=2);break;case 2:t=d.b+t*d.c,(t<=f||n<=0)&&(t=f,b=3);break;case 3:t=f;break;case 4:t=p.b+t*p.c,(t<=0||l<=0)&&(t=0,b=0)}e[o]=t*h+y}}function D(e){let r=P(e.gate[0]);r===!0?b=1:r===!1&&(b=4),x(e.attack[0],e.decay[0],e.sustain[0],e.release[0]),y=e.offset[0],h=e.gain[0]}function x(e,r,o,s){f!==o&&(f=o,n=r,m(d,n,f,c)),i!==e&&(i=e,m(g,i,1+2*a,a)),n!==r&&(n=r,m(d,n,f,c)),l!==s&&(l=s,m(p,l,0,c))}function m(e,r,o,s){let R=r*u;e.c=Math.exp(-Math.log((1+s)/s)/R),e.b=(o-s)*(1-e.c)}}var A=class extends AudioWorkletProcessor{p;g;r=!0;constructor(a){super(),this.g=a?.processorOptions?.mode!=="Modifier",this.p=F(sampleRate),this.port.onmessage=c=>{switch(c.data.type){case"STOP":this.r=!1;break}}}process(a,c,i){let n=c[0][0],l=a[0][0];return this.g?this.p.agen(n,i):l&&this.p.amod(l,n,i),this.r}static get parameterDescriptors(){return[["gate",0,0,1],["attack",.01,0,1],["decay",.1,0,1],["sustain",.5,0,1],["release",.3,0,1],["offset",0,0,2e4],["gain",1,-2e4,2e4]].map(([a,c,i,n])=>({name:a,defaultValue:c,minValue:i,maxValue:n,automationRate:"k-rate"}))}};registerProcessor("AdsrWorkletProcessor",A);})();`;
export const PROCESSOR = `"use strict";(()=>{function C(){let i=!1;return a=>{if(i===!1&&a>=.9)return i=!0,!0;if(i===!0&&a<.1)return i=!1,!1}}function x(i){let a=Math.exp(-1.5),c=Math.exp(-4.95),u=0,n=0,l=0,f=0,h=0,y=1,g={b:0,c:0},m={b:0,c:0},p={b:0,c:0},D=C(),b=0,t=0;return F(.01,.1,.5,.3),{agen:k,amod:P};function P(e,r,o){k(r,o);for(let s=0;s<r.length;s++)r[s]*=e[s]}function k(e,r){R(r);for(let o=0;o<e.length;o++){switch(b){case 1:t=g.b+t*g.c,(t>=1||u<=0)&&(t=1,b=2);break;case 2:t=m.b+t*m.c,(t<=f||n<=0)&&(t=f,b=3);break;case 3:t=f;break;case 4:t=p.b+t*p.c,(t<=0||l<=0)&&(t=0,b=0)}e[o]=t*y+h}}function R(e){let r=D(e.gate[0]);r===!0?b=1:r===!1&&(b=4),F(e.attack[0],e.decay[0],e.sustain[0],e.release[0]),h=e.offset[0],y=e.gain[0]}function F(e,r,o,s){f!==o&&(f=o,n=r,d(m,n,f,c)),u!==e&&(u=e,d(g,u,1+2*a,a)),n!==r&&(n=r,d(m,n,f,c)),l!==s&&(l=s,d(p,l,0,c))}function d(e,r,o,s){let $=r*i;e.c=Math.exp(-Math.log((1+s)/s)/$),e.b=(o-s)*(1-e.c)}}var A=class extends AudioWorkletProcessor{p;g;r=!0;constructor(a){super(),this.g=a?.processorOptions?.mode!=="modulator",this.p=x(sampleRate),this.port.onmessage=c=>{switch(c.data.type){case"DISCONNECT":this.r=!1;break}}}process(a,c,u){let n=c[0][0],l=a[0][0];return this.g?this.p.agen(n,u):l&&this.p.amod(l,n,u),this.r}static get parameterDescriptors(){return[["gate",0,0,1],["attack",.01,0,1],["decay",.1,0,1],["sustain",.5,0,1],["release",.3,0,1],["offset",0,0,2e4],["gain",1,-2e4,2e4]].map(([a,c,u,n])=>({name:a,defaultValue:c,minValue:u,maxValue:n,automationRate:"k-rate"}))}};registerProcessor("AdsrWorkletProcessor",A);})();`;
2 changes: 1 addition & 1 deletion packages/noise/src/processor.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const PROCESSOR = `"use strict";(()=>{var e=class extends AudioWorkletProcessor{static parameterDescriptors=[];r;constructor(){super(),this.r=!0,this.port.onmessage=s=>{switch(s.data.type){case"STOP":this.r=!1;break}}}process(s,t,o){for(let r=0;r<t[0][0].length;r++)t[0][0][r]=Math.random()*2-1;return this.r}};registerProcessor("NoiseProcessor",e);})();`;
export const PROCESSOR = `"use strict";(()=>{var e=class extends AudioWorkletProcessor{static parameterDescriptors=[];r;constructor(){super(),this.r=!0,this.port.onmessage=s=>{switch(s.data.type){case"DISCONNECT":this.r=!1;break}}}process(s,t,o){for(let r=0;r<t[0][0].length;r++)t[0][0][r]=Math.random()*2-1;return this.r}};registerProcessor("NoiseWorkletProcessor",e);})();`;
3 changes: 2 additions & 1 deletion packages/synthlet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
},
"dependencies": {
"@synthlet/adsr": "^0.1.0",
"@synthlet/noise": "^0.1.0"
"@synthlet/noise": "^0.1.0",
"@synthlet/wavetable-oscillator": "^0.1.0"
},
"scripts": {
"build": "tsup src/index.ts --sourcemap --dts --format esm,cjs",
Expand Down
19 changes: 18 additions & 1 deletion packages/synthlet/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
import {} from "@synthlet/adsr";
import { registerAdsrWorkletOnce } from "@synthlet/adsr";
import { registerNoiseWorkletOnce } from "@synthlet/noise";
import { registerWavetableOscillatorWorkletOnce } from "@synthlet/wavetable-oscillator";

export { createAdsr, createVca, registerAdsrWorkletOnce } from "@synthlet/adsr";
export { createWhiteNoise, registerNoiseWorkletOnce } from "@synthlet/noise";
export {
createWavetableOscillatorNode,
registerWavetableOscillatorWorkletOnce,
} from "@synthlet/wavetable-oscillator";

export function registerSynthletOnce(context: AudioContext) {
return Promise.all([
registerAdsrWorkletOnce(context),
registerNoiseWorkletOnce(context),
registerWavetableOscillatorWorkletOnce(context),
]);
}
5 changes: 5 additions & 0 deletions packages/wavetable-oscillator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @synthlet/wavetable-oscillator

## 0.1.0

- Initial implementation
11 changes: 11 additions & 0 deletions packages/wavetable-oscillator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# @synthlet/wavetable-oscillator

> A morphing wavetable oscillator module for [synthlet](https://github.com/danigb/synthlet)
Wavetables are from [WaveEdit Online](https://waveeditonline.com/)

## Install

```bash
npm i @synthlet/wavetable-oscillator
```
36 changes: 36 additions & 0 deletions packages/wavetable-oscillator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@synthlet/wavetable-oscillator",
"version": "0.1.0",
"description": "Wavetable Oscillator module for synthlet",
"keywords": [
"modular",
"synthesizer",
"wavetable",
"oscillator",
"synthlet"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"author": "[email protected]",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"jest": "^29.7.0",
"ts-jest": "^29.1.1"
},
"jest": {
"preset": "ts-jest"
},
"scripts": {
"worklet": "esbuild src/worklet.ts --bundle --minify | sed -e 's/^/export const PROCESSOR = \\`/' -e 's/$/\\`;/' > src/processor.ts",
"lib": "tsup src/index.ts --sourcemap --dts --format esm,cjs",
"build": "npm run worklet && npm run lib"
}
}
Loading

0 comments on commit d35ec18

Please sign in to comment.