Screenshot

Tale's JSFX Pack (including mono_synth)

While playing around with the new REAPER v4.25+ JSFX functions I have created a simple free (LGPL) mono/poly synth in JSFX. It features up to 16 notes polyphony, ADSR, several bandlimited waveforms, a zero-delay feedback low-pass filter, a LFO modulating pulse width or frequency, and supports MIDI Note On/Off, Sustain, and Pitch Wheel.


Documentation

Tale's JSFX Pack includes these JSFX plug-ins:

The following JSFX libraries are included:


adsr.jsfx-inc - ADSR envelope

Example

desc:Mono synth with ADSR

slider1:3<0,5000,1>Attack (ms)
slider2:1000<1,15000,1>Decay (ms)
slider3:-12.0<-120.0,24.0,1.0>Sustain (dB)
slider4:500<0,5000,1>Release (ms)

import Tale/adsr.jsfx-inc
import Tale/midi_queue.jsfx-inc
import Tale/poly_blep.jsfx-inc

@slider

adsr.adsr_seta(slider1 * 0.001);
adsr.adsr_setd(slider2 * 0.001);
adsr.adsr_sets(exp(log(10)/20 * slider3));
adsr.adsr_setr(slider4 * 0.001);

@sample

while(midi.midiq_recv()) (
  midi.msg1 &= 0xF0;

  // Note On
  midi.msg1 == 0x90 && midi.msg3 ? (
    osc.poly_setf(note = midi.msg2, 440);
    // Attack
    adsr.adsr_a(0.5 * midi.msg3 / 128);
  ) :

  // Note Off
  midi.msg1 == 0x80 || midi.msg1 == 0x90 ? (
    // Release
    midi.msg2 == note ? adsr.adsr_r();
  );
);

// Process ADSR envelope and apply it to oscillator.
adsr.adsr_process() ? spl0 = spl1 = adsr.env * osc.poly_saw();

Setting Functions

Processing Functions

Instance Variables


array.jsfx-inc - Simple two-dimensional array interface

Example

desc:Last-note priority mono synth

import Tale/array.jsfx-inc
import Tale/midi_queue.jsfx-inc
import Tale/poly_blep.jsfx-inc

@init

voice.array_init(0, 128, 2);

@sample

while(midi.midiq_recv()) (
  midi.msg1 &= 0xF0;

  // Note On
  midi.msg1 == 0x90 && midi.msg3 ? (

    // Remove note if somehow it is already playing.
    ptr = voice.array_find(midi.msg2);
    ptr >= 0 ? voice.array_remove(ptr);

    // Add note, and set pointer to it.
    ptr = voice.array_add();
    ptr[0] = midi.msg2;

    // Set oscillator frequency.
    ptr[1] = osc.poly_setf(midi.msg2, 440);
    osc.a *= 0.5;
  ) :

  // Note Off
  midi.msg1 == 0x80 || midi.msg1 == 0x90 ? (

    // Remove note.
    ptr = voice.array_find(midi.msg2);
    ptr >= 0 ? (
      voice.array_remove(ptr);
      !voice.size ? osc.a = 0 : (

        // Update pointer to new last note.
        ptr = voice.array_get(voice.size - 1);
        osc.poly_setdt(ptr[1]);
        osc.a *= 0.5;
      );
    );
  ) :

  // All Notes Off
  midi.msg1 == 0xB0 && midi.msg2 == 123 ? (
    voice.array_clear();
  );
);

spl0 = spl1 = osc.poly_saw();

Initialization Functions

Array Functions

Miscellaneous Functions

Instance Variables


complex.jsfx-inc - Complex math operations

Setting Functions

Polar Functions

Equality Functions

Elementary Functions

Exponentiation Functions

Trigonometric functions

Instance Variables


fft_real_synth.jsfx-inc - Real FFT bandlimited synthesis

Uses the real instead of the complex FFT, which is almost 2x as fast, but requires REAPER v5.25+. See Tale/fft_synth.jsfx-inc for more information.


fft_synth.jsfx-inc - FFT bandlimited synthesis

Example

desc:FFT sawtooth oscillator

slider1:440<20,20000,1>Freq (Hz)

import Tale/fft_synth.jsfx-inc
import Tale/wavetable.jsfx-inc

@init

function render_saw(buf, size, gain)
  local(x, dx)
(
  x = -gain;
  dx = 2 * gain / (size - 1);
  loop(size,
    buf[] = x;
    buf += 1;
    x += dx;
  );
);

osc.four_init(0, 1024);
render_saw(osc.buf, osc.size, 0.25);
osc.four_fft();

@slider

osc.four_setf(slider1);
osc.four_update() ? osc.four_ifft();

@sample

spl0 = spl1 = osc.wave_lerp();

Initialisation Functions

Setting Functions

FFT Functions

Miscellaneous Functions

Instance Variables


fourier_series.jsfx-inc - Fourier series waveforms

Example

desc:Fourier series square wave oscillator

slider1:440<20,20000,1>Freq (Hz)
slider2:0.5<0.0,1.0,0.01>Pulse Width

import Tale/fft_synth.jsfx-inc
import Tale/fourier_series.jsfx-inc
import Tale/wavetable.jsfx-inc

@init

osc.four_init(0, 1024);

@slider

osc.four_setf(slider1);
osc.four_setpw(slider2);

osc.four_update() ? (
  osc.four_rect();
  osc.four_setdc(0);
  osc.four_ifft();
);

@sample

spl0 = spl1 = 0.25 * osc.wave_lerp();

Setting Functions

Waveform Functions

Miscellaneous Functions

Instance Variables


lfo.jsfx-inc - Naive (non-bandlimited) low-frequency oscillator

Example

desc:Tremolo

slider1:4.0<0.0,20.0,0.1>Rate (Hz)
slider2:-9.0<-24.0,0.0,0.1>Amount (dB)

import Tale/lfo.jsfx-inc

@slider

lfo.lfo_setf(slider1);
amount = 0.5*(exp(log(10)/20 * slider2) - 1);

@sample

trem = amount * (lfo.lfo_tri() + 1) + 1;
spl0 *= trem;
spl1 *= trem;

Setting Functions

Waveform Functions

Miscellaneous Functions

Instance Variables


malloc.jsfx-inc - Dynamic memory management

Example

desc:Delay

slider1:300<0,1000,1>Delay (ms)
slider2:-6.0<-72.0,0.0,0.1>Feedback (dB)
slider3:-6.0<-72.0,0.0,0.1>Wet Mix (dB)

import Tale/malloc.jsfx-inc

@slider

function resize(new_len)
  global(buf, len, idx)
(
  buf = realloc(buf, new_len);
  new_len > len ? memset(buf + len, 0, new_len - len);

  idx %= new_len;
  len = new_len;
);

resize(max(floor(slider1 * 0.001 * srate), 1));
fb = exp(log(10)/20 * slider2);
mix = exp(log(10)/20 * slider3);

@sample

buf[idx] = 0.5*(spl0 + spl1) + fb * buf[idx];
idx = (idx + 1) % len;

out = mix * buf[idx];
spl0 += out;
spl1 += out;

Memory Management Functions


midi_queue.jsfx-inc - Sample accurate MIDI queue

Example #1 (simple interface)

desc:Simple MIDI mono synth

import Tale/midi_queue.jsfx-inc
import Tale/poly_blep.jsfx-inc

@sample

// Receive MIDI messages.
while(midi.midiq_recv()) (

  // Parse MIDI message.
  midi.msg1 &= 0xF0;

  // Note On
  midi.msg1 == 0x90 && midi.msg3 ? (
    osc.poly_setf(note = midi.msg2, 440);
    osc.a *= 0.5 * midi.msg3 / 128;
  ) :

  // Note Off
  midi.msg1 == 0x80 || midi.msg1 == 0x90 ? (
    midi.msg2 == note ? osc.a = 0;
  );
);

// Sawtooth oscillator.
spl0 = spl1 = osc.poly_saw();

Example #2 (queue interface)

desc:MIDI queue mono synth

import Tale/midi_queue.jsfx-inc
import Tale/poly_blep.jsfx-inc

@init

// Set MIDI queue local memory index and size.
midi.midiq_init(0, 1024);

@block

// Receive MIDI messages and add them to queue.
midi.midiq_collect();

@sample

// Remove MIDI message from the head of queue.
while(midi.midiq_remove()) (

  // Parse MIDI message.
  midi.msg1 &= 0xF0;

  // Note On
  midi.msg1 == 0x90 && midi.msg3 ? (
    osc.poly_setf(note = midi.msg2, 440);
    osc.a *= 0.5 * midi.msg3 / 128;
  ) :

  // Note Off
  midi.msg1 == 0x80 || midi.msg1 == 0x90 ? (
    midi.msg2 == note ? osc.a = 0;
  );
);

// Sawtooth oscillator.
spl0 = spl1 = osc.poly_saw();

Simple Interface

Initialization Functions

Queue Functions

Miscellaneous Functions

Instance Variables


noise.jsfx-inc - Noise generator

Example

desc:Pink noise generator

import Tale/noise.jsfx-inc

@sample
spl0 = spl1 = 0.021 * rng.lcg_pink();

Setting Functions

Noise Functions

Miscellaneous Functions

Instance Variables


oversample.jsfx-inc - 2x oversampler

Example

desc:Oversampled distortion

import Tale/oversample.jsfx-inc

@init

function tanh(x)
(
  x = exp(2*x);
  (x - 1) / (x + 1);
);

pdc_bot_ch = 0; pdc_top_ch = 2;
pdc_delay = 3;

@slider

drive = exp(log(10)/20 * slider1);
gain = sqrt(1/drive);

@sample

os.os_up2(0.5*(spl0 + spl1));

os.y1 = tanh(drive * os.y1);
os.y0 = tanh(drive * os.y0);

spl0 = spl1 = gain * os.os_down2();

Upsample/Downsample Functions

Miscellaneous Functions

Instance Variables


poly_blep.jsfx-inc - PolyBLEP quasi-bandlimited tone generator

Example

desc:Bandlimited sawtooth oscillator
slider1:440<20,20000,1>Freq (Hz)

import Tale/poly_blep.jsfx-inc

@slider
osc.poly_setf(slider1);

@sample
spl0 = spl1 = 0.25 * osc.poly_saw();

Setting Functions

Waveform Functions

Miscellaneous Functions

Instance Variables


rbj_filter.jsfx-inc - 2nd-order RBJ filter

Example

desc:Low-pass filter

slider1:1000<20,20000,1>Cutoff (Hz)
slider2:0.5<0.01,4.0,0.01>Q

import Tale/rbj_filter.jsfx-inc

@slider
lp.rbj_lp(slider1, slider2);

@sample
spl0 = spl1 = lp.rbj_df1(0.5*(spl0 + spl1));

Setting Functions

Filter Functions

Miscellaneous Functions

Instance Variables


rc_filter.jsfx-inc - 1st-order RC filter

Example

desc:RC filter
slider1:1000<20,20000,1>Cutoff (Hz)

import Tale/rc_filter.jsfx-inc

@slider
lp.rc_setf(slider1);

@sample
spl0 = spl1 = lp.rc_lp(0.5*(spl0 + spl1));

Setting Functions

Filter Functions

High Precision Functions

Instance Variables


sine.jsfx-inc - Efficient sine/cosine wave oscillator

Example

desc:Sine/cosine wave oscillator

slider1:440<20,12000,1>Freq (Hz)
slider2:0<0,1,1{Sine,Cosine}>Wave

import Tale/sine.jsfx-inc

@slider
osc.sin_setf(slider1);

@sample
spl0 = spl1 = 0.25 * (slider2 < 0.5 ? osc.sin_sin() : osc.sin_cos());

Setting Functions

Waveform Functions

Miscellaneous Functions

Instance Variables

Correctional Facilities


uint.jsfx-inc - Low-level 32/64-bit unsigned integer operations

Example

desc:Xorshift noise generator

import Tale/uint.jsfx-inc

@init

// Source: https://en.wikipedia.org/wiki/Xorshift#xorshift.2A

state.hw = 0; state.lw = 1;
mul.hw = 0x2545F491; mul.lw = 0x4F6CDD1D;

@sample

x.hw = state.hw; x.lw = state.lw;

a.shr64(x, 12); x.xor64(x, a);
b.shl64(x, 25); x.xor64(x, b);
c.shr64(x, 27); x.xor64(x, c);

state.hw = x.hw; state.lw = x.lw;

x.mul64(x, mul);
spl0 = spl1 = ((x.hw * 2^(32 - 63) + x.lw * 2^(-63)) - 1) * 0.125;

32-Bit Functions

64-Bit Functions

Instance Variables


wavetable.jsfx-inc - Wavetable oscillator

Example

desc:Sine wave wavetable oscillator

slider1:440<20,20000,1>Freq (Hz)
slider2:1<0,2,1{Truncate,Linear,Spline}>Mode

import Tale/wavetable.jsfx-inc

@init

function render_sin(buf, size, gain)
  local(x, dx)
(
  x = 0;
  dx = 2*$pi / size;
  loop(size,
    buf[] = gain * sin(x);
    buf += 1;
    x += dx;
  );
);

osc.wave_init(0, 16);
render_sin(osc.buf, osc.size, 0.25);

@slider

osc.wave_setf(slider1);
mode = slider2|0;

@sample

spl0 = spl1 =
mode == 0 ? osc.wave_trunc() :
mode == 1 ? osc.wave_lerp() :
mode == 2 ? osc.wave_spline3();

Initialisation Functions

Setting Functions

Interpolation Functions

Miscellaneous Functions

Instance Variables


window.jsfx-inc - Collection of window functions

Example

desc:Low-pass windowed FIR filter
slider1:1000<20,20000,1>Cutoff (Hz)
slider2:1<0,1,1{Rectangle,Blackman}>Window

import Tale/window.jsfx-inc

@init

fir_len = floor(64 / 44100 * srate + 0.5);

in_ptr = in_buf = 0;
fir_buf = in_buf + fir_len * 2;

pdc_bot_ch = 0; pdc_top_ch = 2;
pdc_delay = floor(fir_len / 2);

@slider

// Source: http://www.labbookpages.co.uk/audio/firWindowing.html

m = fir_len - 1;
ft = min(0.5, slider1 / srate);

n = avg = 0;
loop(fir_len,
  // Low-pass filter.
  lpf = n != m/2 ? sin(2*$pi*ft*(n - m/2)) / ($pi*(n - m/2)) : 2*ft;

  // Apply window.
  slider2 >= 0.5 ? lpf *= wnd_blackman(n, fir_len);
  avg += lpf;

  fir_buf[n] = lpf;
  n += 1;
);

scale = 1/avg;

@sample

// Buffer input.
(in_ptr -= 1) < in_buf ? (
  in_ptr = in_buf + fir_len;
  memcpy(in_ptr + 1, in_buf, m);
);
in_ptr[] = 0.5*(spl0 + spl1);

// Convolve with FIR filter.
sum = n = 0;
loop(fir_len,
  sum += fir_buf[n] * in_ptr[n];
  n += 1;
);

spl0 = spl1 = sum * scale;

Window Functions

Miscellaneous Functions


zdf_filter.jsfx-inc - 2nd-order zero-delay feedback state variable filter

Example

desc:Low-pass filter

slider1:1000<20,20000,1>Cutoff (Hz)
slider2:0.5<0.01,4.0,0.01>Q

import Tale/zdf_filter.jsfx-inc

@slider
lp.zdf_lp(slider1, slider2);

@sample
spl0 = spl1 = lp.zdf_svf(0.5*(spl0 + spl1));

Setting Functions

Filter Functions

Miscellaneous Functions

Instance Variables