Document: | Cycle Sample Generator / Soft Synth
Author: | bsp
Date: | 16-Dec-2023

# 1 Table of Contents

# 2 Preface

`Cycle` is a modular soft-synth which emits `TKS` or `C` sample generation code, or STFX plugins.
It operates either in hi-fi (32bit float), or lo-fi (8bit signed integer) mode:

- hi-fi uses 32bit floating point calculations while lo-fi uses 16bit fixed point calculations with signed 8bit sample output
- in lo-fi mode, `Cycle` can be used to calculate sample data for Amiga MOD or TSR (Tiny Synergy Replay) files.
- lo-fi sub-trees can be embedded in hi-fi patches (see int).

While the processing blocks (modules) in `Cycle` are rather simple, their combination can yield quite complex sounds, even with (by today's standards, slow) CPUs like the Motorola M68000.
In STFX plugin-mode, `Cycle` generates native-code `C` voice plugins via the GCC / Clang compiler.
STFX plugins and float sample generation code can also be used in the standalone Synergy replay.

# 3 Global Keywords

Global keywords may appear at the beginning of a Cycle script.

Keyword | Description
--- | ---
arg | Declare argument (will be editable in UI)
var | Declare variable
oversample_factor oversample | Oversample factor (0.03125..32, def=1) (also supports fractional values, e.g. "1/8")

| Oversampling is ignored when exporting to lo-fi 8bit "C" sample generator source

| 8 is usually a good factor when targeting ~16kHz samples (=128kHz internal rate)

oversample_quality | Oversample quality (0..10, def=4)

| Oversampling is ignored when exporting to lo-fi 8bit "C" sample generator source

rate | Override zone sample-rate
freq | Output frequency. Default is 261.63 (middle C, MIDI note 60 / C-5). alternatively: MIDI note C-1..G-A (followed by finetune, e.g. `G-4 -0.2`) alternatively: float ratio, e.g. 16574/64
oct | Octave shift (-4..4)
dur | Override sample duration (frames or, when followed by "ms", milliseconds)
wt_w | Override wavetable width (1..16 in 2D mode, 1..128 in 1D mode)
wt_h | Override wavetable height (1..16 in 2D mode, 1 in 1D mode)
wt_cyclelen | Override wavetable cycle length (number of frames. 1..1048576)
wt_freqs | Per-cycle note frequencies/rates (multisampled keymaps) e.g. `wt_freqs c-3 g-3 c-4 "g-4+0.11" c-5 g-5 c-6 g-6 c-7 g-7`

| the current cycle freq is available via `$wt_freq` variable during synthesis

skip | Skip output of first "n" sample frames or, when followed by "ms", milliseconds
xfade | Crossfade end of loop with samples before loop start. Sample frames or loop-len percentage (when followed by %)
mirror | Calc first half waveform and mirror to second half (single cycle waveforms and wavetables)
curve | Declare curve lookup-table. Must be followed by curve index (0..15) and curve id (e.g. "amp")
lut | Declare procedural lookup-table. Must be followed by LUT id, optional mode (voice/shared), optional table size (default=256), and optional "inv" (inverse) flag
zone | Declare external zone reference. Must be followed by ref idx (0..) and zone id (e.g. "myzone", or "myothersmp.myzone")
macro | Declare macro parameter set(s). See macros.

Global Keywords

## 3.1 Plugin keywords

Keywords used in plugin-generation mode:

Keyword | Description
--- | ---
name | Plugin name

| the plugin name (and dll / so / dylib) will be prefixed with `cycle_`

author | Plugin author
cat category | Plugin category

| unknown | limiter | pan | transient
| ringmod | enhance | pitch | convolver
| waveshaper | dac | chorus | mixer
| filter | lofi | flanger | comparator
| eq | gate | delay | utility
| compressor | amp | reverb | osc

Plugin Keywords

# 4 Values and sample output

The same value type is used for module parameters, args, variables, and intermediate sample data.
In hi-fi mode, values are represented by 32 bit floating point numbers. The final sample output is also a `float`.
In lo-fi mode, values are represented by signed twos-complement, 16 bit fixed point numbers (4:11), i.e. the value range is -15..15.
The final sample output is a signed 8 bit sample.

# 5 Curve Lookup Tables (LUTs)

Cycle supports up to 16 cubic-bezier curve tables that can be used as amp or filter envelopes, or sample waveshapers.
Curve tables are internally converted (sampled) to short, power-of-two sized arrays. The array sizes can be configured in the UI.
Before a curve table can be used with a lut module, it must first be declared with a `curve` statement:

    curve 0 amp

    <out:
      sin
      * rmp 0 0 0.99
        lut amp

Using a curve LUT as an amp envelope

# 6 Procedural Lookup Tables (LUTs)

Cycle supports an arbitrary number of procedural look up tables which share the same namespace with curve lookup tables.
The table size must be a power of two.
Tables are calculated either per-voice (at note-on, using the then-current parameters and modulation), or statically (shared among all voices).

    lut pow shared 256

    <lut_pow:
      # (note) implicit var "x" stores normalized table position (0..1=last table element)
      # (note) the default out value is $x
      fma 2 -1
      pow 1.44

    <out:
      sin
      fma 0.5 0.5
      lut pow lin=1

shared LUT example

    param p_exp 1 1 16

    mod m_exp

    lut pow voice 32

    <lut_pow:
      # (note) implicit var "x" stores normalized table position (0..1=last table element)
      # (note) the default out value is $x
      fma 2 -1
      pow exp=$m_exp

    <out:
      sin
      fma 0.5 0.5
      lut pow lin=1

per-voice modulated LUT example

| lookup tables can greatly improve the rendering performance by pre-calculating recurring values

| the default table size is 64

| the default table mode is "voice"

| LUT init sections must be named "lut_<id>"

| if no init section has been declared for a LUT used by a "lut" module, compilation will fail

| the shared LUTs are calculated after the "<init:" section

| the per-voice (or per-WT cycle) LUTs are updated after the "<prepare:" / "<wt_init:" section

## 6.1 Inverse Lookup Tables

See sta module documentation for examples.

# 7 Macros

Macros are used to define smoothly interpolated value sets, controlled by a single parameter (or other value).
They are mostly useful to hide the internal complexity of a Cycle patch, and reduce the number of (plugin-)parameters.
The mac module updates the current, interpolated values of the macro fields.
The current value of a macro field is accessed (read) using the `<macro_id>.<field_name>` syntax.
Examples:

    id     osc_mix_macro_manual
    name   "osc mix macro manual"
    author bsp
    cat    osc

    # Plugin parameter
    param p_osc_mix 0 0 1

    # 4 manually configured settings
    macro osc_mix
      sin   tri  saw   pul
      1     0     0    0
      0     1     0    0
      0     0     1    0
      0     0     0    1

    <prepare:
      # calculate interpolated macro values
      #  (updated 1000 times per second)
      mac osc_mix pos=$p_osc_mix

    <out:
      # calculate current output sample
      sin
      * osc_mix.sin
      + tri
        * osc_mix.tri
      + saw
        * osc_mix.saw
      + pul
        * osc_mix.pul

macro example (manual settings)

    id     osc_mix_macro_permutate
    name   "osc mix macro permutate"
    author bsp
    cat    osc

    # Plugin parameter
    param p_osc_mix 0 0 1

    # auto-generated settings
    #  (permutations of values 0/1/-1 => 3*3*3*3=81 settings)
    macro osc_mix
      sin   tri  saw   pul
      permutate 0 1 -1

    <prepare:
      # calculate interpolated macro values
      #  (updated 1000 times per second)
      mac osc_mix $p_osc_mix

    <out:
      # calculate current output sample
      sin
      * osc_mix.sin
      + tri
        * osc_mix.tri
      + saw
        * osc_mix.saw
      + pul
        * osc_mix.pul

macro example (permutated settings)
`pos` is in the range 0..1 (last setting is lerped back to first setting)

| the maximum number of (auto-generated) permutations is limited to 16384

| alternative to macros, the `<init:` and `<prepare:` sections may also contain arbitrarily complex, procedural pre-calculations

# 8 Output lanes

Lanes divide the script into multiple output sections.
Each script must contain the main section (named `out`):

    <out:
      sin

## 8.1 Layers

When a script contains multiple lanes, they will be summed to create the final sample output.
This can be used for layered synthesis (lane == layer).

## 8.2 Init lane

A script may contain an optional `init` section that is evaluated before the main section.
It can be used to implement procedural macro parameters that read args and write one or many variables.

    arg angle 0 0 1
    var v_sin
    var v_cos
    <init:
      sin freq=0 phase=$angle
      sto v_sin
      sin freq=0
        phase:
          $angle
          + 0.25
      sto v_cos

sin / cos init example

| the `init` section must occur before the first audio lane

| in plugin-mode, the `init` section is evaluated each time a new note is played (note-on)

## 8.3 Prepare lane

The `prepare` section is evaluated before the main section(s), and after the (optional) `init` section.
Heavy calculations, or calculations which do not need to be evaluated per sample frame should be placed in the `prepare` section.

    id     simple_2op_fm_osc
    name   "simple_2op_fm_osc"
    author bsp
    cat    osc

    param p_op2_ratio 0.5 0 1
    param p_op2_level 0 0 16

    mod m_op2_ratio
    mod m_op2_level

    var v_op2_freq

    <prepare:
      # ratio 0..1 => (1/4096)³ .. 1 .. 4³
      $m_op2_ratio
      # 0..1 => -1..1
      fma 2 -1
      # -1=>div 4096, +1=>mul 4
      bts 4096 4
      pow 3
      # quantize to 100 fractional divisions per integer
      qua 100
      sto v_op2_freq

    <out:
      sin
        phase:
          sin freq=$v_op2_freq
          * $m_op2_level

simple 2 operator FM / phase modulation oscillator plugin

| in plugin-mode, the `prepare` section is called at a rate of 1kHz while a voice is playing.

## 8.4 Wavetable Cycle Init lane

The optional `wt_init` section is evaluated at the beginning of each wavetable cycle.
It can be used to implement procedural macro parameters that read the built-in `$wt_x` / `wt_y` vars and write one or many variables.

    arg angle_scl 1 0 15
    arg angle_off 0 -1 1
    var v_sin
    var v_cos
    <wt_init:
      sin freq=0
        phase:
          $wt_x
          * $angle_scl
          + $angle_off
      sto v_sin
      sin freq=0
        phase:
          $wt_x
          * $angle_scl
          + $angle_off
          + 0.25
      sto v_cos

sin / cos wavetable cycle init example

| the `wt_init` section must occur before the first audio lane, and after the (optional) `init` lane

# 9 Constants

Constants are typically used for static module parameters, e.g.

    <out:
      sin freq=1.5

A constant is either a floating point number (in the range -15..15), or a note ratio in the form of `d#3/g-3`:

    <out:
      sin freq=d#4/c-3

# 10 Args

Args are named constants that are declared by the global keyword `arg` (before any output section).
Arg values are editable in the (auto-generated) script UI (slider + value widgets).
Example:

    arg lpf1_ms 30 min=0 max=500
    arg lpf1_ms 30 0 500
    arg lpf_rng 0.3

The default (implicit) min / max range is 0..1
Script code can reference args by prefixing the arg id with a `$` sign, e.g.

    arg amp_lvl 1.5

    <out:
      sin
      * $amp_lvl
      clp

| args and variables share the same namespace

## 10.1 Arg Preset Values

Each arg can be assigned a list of preset values (shown when hold-clicking the value widget).
Example:

    arg angle 0 0 1

    arg_values angle 0="0 deg" 0.25="90 deg" 0.5="180 deg" 0.75="270" 1="360 deg"

| arg preset values always refer to scaled values, i.e. within min..max range

# 11 Variables

Variables are very similar to args.
While they do not generate UI widgets, their content may be updated at any time via the sto module.
Script code can read variables by prefixing the var id with a `$` sign, e.g.

    var myvar

    <out:
      2.5
      sto myvar
      sin
      * $myvar
      clp

| args and variables share the same namespace

## 11.1 Built-in Variables

`$x` stores the current, normalized position in procedural look-up tables (lut)
`$wt_x` stores the current, normalized horizontal (or 1D) wavetable position (0..1)
`$wt_y` stores the current, normalized vertical (2D) wavetable position (0..1)

# 12 Operators

Each module may be prefixed by an operator that determines how its output is combined with the previous module's output.

Operator | Id | Description
--- | --- | ---
= | OP_REP | Replace
+ | OP_ADD | Add
- | OP_SUB | Subtract (a-b)
r | OP_RSUB | Reverse-Subtract (b-a)
* | OP_MUL | Multiply
& | OP_AND | Bitwise and
| | OP_OR | Bitwise or
^ | OP_EOR | Bitwise exclusive-or
m | OP_MIN | Minimum
x | OP_MAX | Maximum
M | OP_ABSMIN | Absolute minimum
X | OP_ABSMAX | Absolute maximum
. | OP_NONE | Discard / Ignore

Operators
The default operator is `replace`.

# 13 Modules

Modules generate new sample frames or process the output of the previous module.
Modules may be nested (stacked), i.e. module inputs can be controlled by the output of other modules.
Example:

    <out:
      sin
        freq:
          sin 5.0
          * 0.25
          + 1
      * 3.0
      clp

Static module parameters are set by `key=value` pairs, or simply by value (in internal parameter order)

      lpf freq=0.4 res=0.5
      lpf 0.4 0.5

Dynamic module parameters must be placed on separate, indented lines.
Please note that some module parameters cannot be dynamic (e.g. `hpf` filter configuration).
Also keep in mind that dynamic parameters require more complex code that will lengthen the synthesis duration.

## 13.1 abs

Return absolute value of previous output.
Example:

    <out:
      -42
      abs

## 13.2 bit

Bit-crush (shift) previous output.
Example:

    <out:
      sin
      bit shift=0.5
      bit 0.5

`shift` range is 0..1

## 13.3 boo

Boost sample deltas of previous output.
Example:

    <out:
      sin
      boo amount=0.6
      boo 0.6

`amount` range is 0..1

## 13.4 box

Box filter previous output.
Example:

    <out:
      saw
      box freq=0.5
      box 0.5

`freq` range is 0..1

## 13.5 bpf

not implemented, yet

| use svf in mode=2 instead

## 13.6 bts

Bipolar to scale.
Example:

    arg $freq
    <out:
      sin
        freq:
          $freq
          # 0..1 => -1..1
          fma 2 -1
          # -1..1 => 1/16 .. *16
          bts 16

`div` range is 1..32 [def=8]
`mul` range is 1..32 [def=8]

| uses `div` input when `mul` is not connected

| only supported in float-mode (fall back to float in int mode)

## 13.7 buf

not implemented, yet

## 13.8 bus

Read sample from (cross-zone) voicebus.
Example:

    arg bus 0 0 0.32
    arg lvl
    <out:
       sin
         phase:
           bus nr=$bus
           * $lvl

phase-modulate sine wave by previous zone output
`nr` is in the range 0..0.32 (bus 1..32, 0=previous zone nr)

| only supported in STFX plugin mode (returns 0 otherwise)

## 13.9 clp

Clip previous output
Examples:

    <out:
      sin
      * 7
      clp

clip to -1..+1

    <out:
      sin
      * 7
      clp 0.5

clip to -0.5..+0.5

    <out:
      sin
      * 7
      clp floor=-0.75 ceil=0.6

separate boundaries
`ceil` range is 0..1 (default=1)
`floor` range is -1..1 (default=-1) (when not connected, floor=-ceil)

## 13.10 dec

Decrement variable

    var f
    <out:
      set f 0
      rep 16
        dec f 0.1
        dec f delta=0.1
        dec f
          delta:
            0.05
            * 2

## 13.11 div

Divide constant values.
Example:

    arg p_num 16 1 32
    <out:
      div 1.0 $p_num

| useful in conjunction with `rep` / `lop`, e.g. when indexing LUTs

## 13.12 dly

not implemented, yet

## 13.13 drv

not implemented, yet

## 13.14 end

Close current lane.

| optional. can be used for placing lut declarations in between lanes.

| when lane is a lut init lane, skip the output write (lane must write to lut via sta instead)

## 13.15 env

not implemented, yet, may be removed
(use rmp and lut instead)

## 13.16 fld

Fold previous output value at ceiling.
Example:

    <out:
      sin
      * 2.0
      fld ceil=1.0

| `ceil` sets min=-ceil, max=+ceil

Example:

    <out:
      sin
      * 2.0
      fld min=-1.0 max=1.0

| `min` / `max` override default ceil

| all oscillators can be used as wavefolders by setting their speed to 0, then modulating their phase input

## 13.17 fma

Fused multiply add: `out = out * mul + add`
Example:

    <out:
      sin
      # -1..+1 => 0..1
      fma 0.5 0.5

`mul` input is in range -15..15 [def=2]
`add` input is in range -15..15 [def=-1]

## 13.18 frc

Calc fractional part of previous output (discard integer bits)
Example:

    <out:
      sin
      * 3.3
      frc

| int does the opposite and discards all fractional bits

## 13.19 fsr

Generate linear feedback shift register ("C64") noise (normalized to -1..1).
Example 1:

    <out:
      fsr

Example 2:

    <out:
      fsr seed=$4489 bits=1.0 cycle=1

`seed` is the initial shift register state (16 bits). must not be 0. Default is $4489
`bits` determines the number of output bits (1.0=all bits, 0=1 bit (actually two, sign+msb). Default is 1.0.
`cycle` determines whether the shift register state resets at each (wavetable) cycle. Default is 1 (true).
`shift1` is the first feedback shift (negative=shift left). -15..15. def=7
`shift2` is the second feedback shift. -15..15. def=-9
`shift3` is the third feedback shift. -15..15. def=13
`reset` range is 0..1 (0=disable plugin voice note on reset, default=1)

| changing the number of `bits` does not make an audible difference, except when the output is used as a controller value

| `fsr` noise contains less low frequencies than `nos` noise

## 13.20 hbx

High-pass Box filter previous output.
Example:

    <out:
      saw
      hbx freq=0.5
      hbx 0.5

`freq` range is 0..1

## 13.21 hld

Sample and hold previous output.
Example:

    <out:
      hld rate=0.5

`hld` range is 0..1

| if the previous output uses complex calculations, it is more efficient to render these into a separate zone at a lower rate, then read the precalced values via `zon`

## 13.22 hpf

High-pass filter previous output. Uses preconfigured, static cutoff/resonance configurations (biquad coefficients).
Example:

    <out:
      saw
      hpf cfg=3

### 13.22.1 Filter configurations

cfg | cutoff | resonance
--- | --- | ---
0 | 127 | 0
1 | 127 | 108
2 | 122 | 0
3 | 122 | 108
4 | 115 | 0
5 | 115 | 100
6 | 110 | 0
7 | 110 | 100
8 | 100 | 0
9 | 100 | 97
10 | 90 | 0
11 | 90 | 70
12 | 77 | 0
13 | 77 | 79

## 13.23 if

Conditional code generation (compile time).
Syntax:

      if <arg> [<,<=,==,!=,>=,>] <constant>
        then:
           ..
        else:
           ..

Example:

    arg mod_waveform;

    <out:
      if mod_waveform == 0
        then:
          nos
        else:
          if mod_waveform < 0.33
            then:
              sin
            else:
              if mod_waveform < 0.66
                then:
                  tri
                else:
                  saw

## 13.24 inc

Increment variable

    var f
    <out:
      set f 0
      rep 16
        inc f 0.1
        inc f delta=0.1
        inc f
          delta:
            0.05
            * 2

## 13.25 int

Evaluate sub-tree in integer mode.
The previous output value is converted to 16bit fixpoint (1:4:11) before the sub-tree is emitted.
Afterwards the integer output value is converted back to float.

    <out:
      int
        sin

| in integer mode, no conversions take place and the sub-tree is emitted as usual

## 13.26 kbd

Scales the input by 127 (normalized float to MIDI note), adds `(current_note - 64 + (off * 12))`, clamps to 0..127, then converts the note to its corresponding frequency and divides it by the sample rate (back to normalized 0..1 range).
Useful for scaling filter cutoff frequencies (linear to exponential).

    id     cutoff_kbd_tracking_test
    name   "cutoff kbd tracking test"
    author bsp
    cat    filter

    param p_cutoff 0.5 0 1
    param p_keyfollow 1 0 1
    param p_octave_off 0 -4 4

    <out:
      svf
        freq:
          $p_cutoff
          kbd $p_keyfollow $p_octave_off

filter cutoff keyboard tracking example plugin
`kbd` is in the range -16..16 (keyboard follow amount)
`off` is in the range -4..4 (octave offset)
`scl` is in the range 0..16 (post scaling)
`clamp` toggles clamping to 1.0 (def=true) (should be set to 0 when using this for pitch tracking) (must be const)

| only supported in plugin / float mode (fall back to float within int-subtree)

## 13.27 lle

Scale previous output by logarithmic(c<0)..linear(c=0)..exponential(c>0) factor

    8
    lle c=1.4

`c` is in the range -f..f.

| only supported in float-mode (fall back to float in int mode)

## 13.28 log

Return logarithm of previous output.

    8
    log base=2

`base` is in the range 1..f (must be constant). When input is not connected, return the natural logarithm.

| only supported in float-mode (fall back to float in int mode)

## 13.29 lop

Loop statement sequence. Supports dynamic loop iterators.

    var i
    var num
    <out:
      2.0
      * 4.0
      sto num
      0.0
      sto i
      lop $num
        + 0.01

`num` range is 0..1023

| some modules (e.g. `sin`) use variables to store their state. evaluating such modules in a loop will cause all iterations to use the same state.

| use `rep` to generate unrolled loops where each iteration will have its own state

## 13.30 lpf

1RC low pass filter

    <out:
      saw
      lpf freq=0.4 res=0.5
      lpf 0.4 0.5

`freq` range is 0..1
`res` range is 0..1

## 13.31 lut

Use previous output as index for curve look-up table (LUT)
Example:

    curve 0 amp

    <out:
      sin
      * rmp 0 0 0.99
        lut amp clamp=0 lin=0

`clamp` must be either 0 (repeat lut) or 1 (clamp to last index)
`lin` must be either 0 (nearest neighbour) or 1 (linear interpolation)

| the `lut` module is often combined with a rmp module that generates the look-up index

## 13.32 mac

The `mac` module updates the current, interpolated values of a macro's fields.
See macros for further information and examples.
`pos` is in the range 0..1 (last setting is lerped back to first setting)

## 13.33 neg

Negate previous output
Example:

    <out:
      saw
      neg

## 13.34 nos

Render white noise via xor-shift pseudo random number generator
Example:

    <out:
      nos seed=0x44894489 cycle=0

`seed` is the random seed (a 32bit integer). the default seed is `0x3d9fb971`
`cycle` range is 0..1 (0=disable wavetable cycle reset, default=1)
`reset` range is 0..1 (0=disable plugin voice note on reset, default=1)

## 13.35 pha

Generate phase output (0..1 normalized saw-wave).
Example:

    curve 0 mywaveshape

    <out:
      pha freq=1.5 phase=0.25
      lut mywaveshape

`freq` range is 0.25..4
`phase` range is 0..1 (start phase)
`cycle` range is 0..1 (0=disable wavetable cycle reset, default=1)
`reset` range is 0..1 (0=disable plugin voice note on reset, default=1)
`fixed` range is 0..1 (1=enable fixed frequency mode, default=0)

## 13.36 p2s

Return `(pow(2, exp * prev_out) - 1.0) / (pow(2, exp) - 1.0)`.
Can be used for e.g. scaling filter cutoff frequencies (linear to exponential).
Example:

    arg cutoff

    <out:
      svf
        freq:
          $cutoff
          p2s 7

(non-kbd-tracked) exponential cutoff scaling
When the `kbd` (and optionally the `off`) input is connected and Cycle is in STFX plugin-generation mode, the module input follows the keyboard.

    id     cutoff_kbd_tracking_test_p2s
    name   "cutoff kbd tracking test p2s"
    author bsp
    cat    filter

    param p_cutoff 0.5 0 1
    param p_keyfollow 1 0 1
    param p_octave_off 0 -4 4

    <out:
      svf
        freq:
          $p_cutoff
          p2s kbd=$p_keyfollow off=$p_octave_off

filter cutoff keyboard tracking example plugin
`exp` is in the range 0..15
`scl` is in the range 0..1 0⇒scl=(pow(2,exp-1)) (must be const)
`kbd` is in the range -16..16 (keyboard follow amount)
`off` is in the range -4..4 (octave offset)

| only supported in float-mode (fall back to float in int mode)

| for keyboard tracking purposes, the kbd module is usually the preferred solution

## 13.37 pow

Return previous output raised to the power of 2 or 3 (fast-path), or an arbitrary exponent (float-mode only).
Example:

    <out:
      sin
      pow 3

`exp` range is [2, 3] (int) or any float value

| useful for bending rmp envelopes into non-linear shapes

| constant exponents 2 and 3 behave differently than arbitrary (or dynamic) exponents, i.e. -2²=+4 while e.g. -2^4=-16

| arbitrary or dynamic exponents are only supported in float-mode (fall back to float in int mode)

## 13.38 pul

Generate pulse wave
Example:

    <out:
      pul freq=1 phase=0 width=0.5 vsync=2.5

`freq` range is 0.25..4
`phase` range is 0..1 (start phase)
`width` range is 0..1 (length of duty cycle)
`vsync` range is 0.01..4 (virtual oscillator sync)
`cycle` range is 0..1 (0=disable wavetable cycle reset, default=1)
`reset` range is 0..1 (0=disable plugin voice note on reset, default=1)
`fixed` range is 0..1 (1=enable fixed frequency mode, default=0)
`window` range is 0..3 (0=windowed vsync off, 1=sine window, 2=half-sine window, 3=triangle window) (must be const)

| windowed vsync is only supported in float-mode (ignored in integer mode)

## 13.39 qua

Quantize float value to n fractional steps per integer.

    <out:
      sin
      qua num=10

`num` is in the range 1..n

| useful for controllers, e.g. FM ratios

| when num=1, simply discard fractional bits (cast to integer)

| only supported in float-mode (fall back to float in int mode) (except when num=1)

## 13.40 rep

Loop / unroll statement sequence. The number of iterations must be constant.

    arg p_num 8 1 32
    var i
    <out:
      1.0
      sto i
      0.0
      rep $p_num
        + sin
            freq:
              $i
              * 0.5
          * 0.1
      clp

`num` range is 0..1023

## 13.41 rev

not implemented, yet

## 13.42 rmp

Generate linear ramp
Example:

    <out:
      rmp millisec=8 start=0 end=1 cycle=1

`millisec` range is 0..4000. 0 auto-adjusts the duration to match the target zone sample length (must be const)
`start` range is 0..15 (start value) (must be const)
`end` range is 0..15 (end value) (must be const)
`cycle` determines whether the ramp position is reset at each new wavetable cycle (0 or 1) (must be const)

## 13.43 saw

Generate sawtooth wave
Example:

    <out:
      saw freq=1 phase=0 vsync=2.5

`freq` range is 0.25..4
`phase` range is 0..1 (start phase)
`vsync` range is 0.01..4 (virtual oscillator sync)
`cycle` range is 0..1 (0=disable wavetable cycle reset, default=1)
`reset` range is 0..1 (0=disable plugin voice note on reset, default=1)
`fixed` range is 0..1 (1=enable fixed frequency mode, default=0)
`window` range is 0..3 (0=windowed vsync off, 1=sine window, 2=half-sine window, 3=triangle window) (must be const)

| windowed vsync is only supported in float-mode (ignored in integer mode)

## 13.44 set

Set variable

    var f
    <out:
      set f 1.23
      set f value=1.23
      rep 16
        inc f 0.1

## 13.45 sin

Generate sine wave
Example:

    <out:
      sin freq=1 phase=0 vsync=2.5 window=1

`freq` range is 0.25..4
`phase` range is 0..1 (start phase)
`vsync` range is 0.01..4 (virtual oscillator sync)
`cycle` range is 0..1 (0=disable wavetable cycle reset, default=1)
`reset` range is 0..1 (0=disable plugin voice note on reset, default=1)
`fixed` range is 0..1 (1=enable fixed frequency mode, default=0)
`window` range is 0..3 (0=windowed vsync off, 1=sine window, 2=half-sine window, 3=triangle window) (must be const)

| windowed vsync is only supported in float-mode (ignored in integer mode)

## 13.46 slf

Read past sample output ("self" feedback)

    <out:
      pul
      + slf rate=0.98
      clp

`rate` range is 0..1

## 13.47 ssl

Sine slice oscillator.
Works similar to how the additional 7 waveforms on the Yamaha TX81Z were created back in the 1980ies.
The (built-in) sine table as well as the destination waveform(-cycle) is divided into 8 regions / slices.
Each target slice is assigned one of the 8 sine regions (0..7).

    <out:
      ssl 01234567 freq=1.0 phase=0.0 mode=4 mod=0 seq=0 seqmod=0 modmask=1.0 zeromask=0.0

The first argument is the base slice sequence (0=region 0, 7=region 7).
When the sequence has less than 8 entries, it will be repeated.
`freq` is in the range 0.25..4
`phase` is in the range 0..1
`mod` is in the range -1..1 (slice modulation)
`seq` is in the range 0..1 (0=base slice sequence, 00000001..7777777). Must be const.
`modmask` is in the range 0..1 (0=no slices, 1=all slices)
`zeromask` is in the range 0..1 (0=no slices cleared, 1=all slices cleared)
`mode` is in the range 0..5 (must be const):

Mode | Description
--- | ---
0 point | ignore fractional (mod) bits, simply recombine waveforms (fastest)
1 phase | fractional (mod) bits determine source region start offset shift ("phase" mod 1/8 sin)
2 lerp | fractional (mod) bits are used to lerp sine source region to cosine wave
3 shift | fractional (mod) bits are used to shift source region (dc offset + reflect/fold)
4 ang | fractional (mod) bits determine source waveform start offset shift ("phase" +- 2PI)
5 amp | fractional (mod) bits are used to amplify source region

SLL mode settings
`seqmod` is in the range 0..3 (per-sample sequence modulation)

## 13.48 sta

Store variable in LUT at normalized index determined by previous output value.
Syntax: `sta <lut_id> <index_mul> <index_add> <src_var> <var_mul> <var_add>`
Example:

    lut ws shared 4096
    lut ws_inv shared 4096 inv

    <lut_ws:
      # transform implicit input x={0..1} => {-1..1}
      fma 2 -1
      pow 3
      # store in inverse LUT 'ws_inv'
      #  - transform 'pow' result (-1..1) into normalized index (0..1)
      #  - transform x from 0..1 to -1..1 and store in LUT
      sta ws_inv 0.5 0.5 x 2 -1
      # store 'pow' result in LUT 'ws' (done by auto-generated code)
      #  (note) holes in the ws_inv LUT will be auto-filled afterwards

    <out:
       sin
       # -1..1 => 0..1
       fma 0.5 0.5
       lut ws
       lpf freq=0.3 res=0.5
       clp
       # -1..1 => 0..1
       fma 0.5 0.5
       lut ws_inv

inverse exponential lookup table
Example 2:

    curve 0 ws
    lut ws_inv shared 2048 inv

    <lut_ws_inv:
      # use implicit input x (0..1) as index into bezier curve 'ws'
      lut ws
      clp
      # store in inverse LUT 'ws_inv'
      #  - transform 'clp' result (-1..1) into normalized index (0..1)
      #  - transform x from 0..1 to -1..1 and store in LUT
      sta ws_inv 0.5 0.5 x 2 -1
      # don't write output value to lut (already written)
      end

    <out:
       sin
       # -1..1 => 0..1
       fma 0.5 0.5
       lut ws
       lpf freq=0.3 res=0.5
       clp
       # -1..1 => 0..1
       fma 0.5 0.5
       lut ws_inv

inverse bezier curve lookup table

| referencing an inverse LUT in another LUT's init lane via sta causes the inverse LUT to be filled before the init lane is executed, and potential holes (unwritten elements) in the inverse LUT to be closed afterwards

## 13.49 sto

Store previous output in variable
Example:

    var t

    <out:
      sin
      sto t
      * $t
      * $t

The variable is initialized with 0 but retains its content over multiple sample frame iterations.
Example:

    var sin_val

    <out:
      $sin_val
      + sin
      sto $sin_val
      clp

## 13.50 svf

Process previous output with state-variable filter
Example:

    <out:
      saw
      svf freq=0.4 res=0.5 mode=0
      svf 0.4 0.5 0

`freq` range is 0..1
`res` range is 0..1
`mode` 0=lpf, 1=hpf, 2=bpf, 3=notch

## 13.51 tanh

Process previous output through tangens hyperbolicus (tanh) function.
Example:

    arg drive 0 -1 1

    var v_amp

    <prepare:
      # amp=/10..*10
      10
      pow $drive
      sto v_amp

    <out:
      sin
      * $v_amp
      tanh

tanh

    arg drive 0 0 2

    lut tanh shared 4096

    <lut_tanh:
      # 0..1 => -100..+100
      fma 200 -100
      tanh

    var v_amp

    <prepare:
      # amp=*1..*100
      10
      pow $drive
      sto v_amp

    <out:
      sin
      * $v_amp
      # -100..+100 => 0..1
      fma 1/200 0.5
      lut tanh lin=1

tanh lookup-table

| useful for saturation-like non-linear distortion (converges to -1..1)

| only supported in float-mode (fall back to float in int mode)

## 13.52 trc

Debug-trace last output value (print to console).
Example:

    lut mylut shared 16

    <lut_mylut:
      # print current $x
      trc

    <out:
      sin

## 13.53 tri

Generate triangle wave
Example:

    <out:
      tri freq=1 phase=0 vsync=2.5

`freq` range is 0.25..4
`phase` range is 0..1 (start phase)
`vsync` range is 0.01..4 (virtual oscillator sync)
`cycle` range is 0..1 (0=disable wavetable cycle reset, default=1)
`reset` range is 0..1 (0=disable plugin voice note on reset, default=1)
`fixed` range is 0..1 (1=enable fixed frequency mode, default=0)
`window` range is 0..3 (0=windowed vsync off, 1=sine window, 2=half-sine window, 3=triangle window) (must be const)

| windowed vsync is only supported in float-mode (ignored in integer mode)

## 13.54 vpl

Process previous output via stfx voice plugin.

    <out:
      sin 5.0
      vpl "bsp ws quintic" "Dry / Wet"=0.5 Drive=0.39

Example (single line)

    arg drive 0.39 min=0 max=1

    <out:
      sin 5.0
      vpl "bsp ws quintic" "Dry / Wet"=0.5 "Drive"=$drive

Example (singe line with arg slider)

    arg drive 0.39 min=0 max=1

    <out:
      sin 5.0
      vpl "bsp ws quintic"
        "Dry / Wet" = 1
        "X^0"       = 0
        "X^1"       = 0.5
        "X^2"       = 0.74
        "X^3"       = 0.8
        "X^4"       = 0.62
        "X^5"       = 0.5
        "Drive"     = $drive

Example (multi line with arg slider)
In Lo-Fi (integer) mode, the input/output values are temporarily converted to / from float.

| See Eureka sampler "FX" tab for a list of available plugins.

| Plugin setup can be exported to single- or multi-line Cycle script (clipboard) via Eureka sampler "FX" tab context menu

| `vpl` modules cannot be exported to "C" source

## 13.55 xfd

Crossfade between previous output and input `b`.

    arg mod_waveform 0 -1 1
    <out:
      tri
      xfd amount=$mod_waveform
        b:
          saw

Example (crossfade between triangle and saw)

## 13.56 zlp

Read looped sample frame from other zone. Can be used to mix samples or create chords.
Example:

    zone 0 otherzone
    zone 1 othersmp.otherzone

    <out:
      zlp 0 rate=0.3 phase=0.5
      + zlp 1 0.9
      clp

`rate` range is 0..1
`phase` range is 0..1
`div` range is 0..256 (set repeatlen to zonelen/div) (must be const)
`sub` range is 0..256 (shifts start offset by sub*repeatplen, e.g. for selecting sub-wave from wavetable. requires 'div')
`lin` enables bilinear filtering (def=off) (must be const)

| sample len is determined by last loop length of the specified zone

| sample len should be a power of two (read offset is ANDed with (sampleLen-1))

## 13.57 zon

Read sample frame from other zone. Can be used to mix samples or create chords.
Example:

    zone 0 otherzone
    zone 1 othersmp.otherzone

    <out:
      zon 0 rate=0.3
      + zon 1 0.9
      clp

Upsampled loop example:

    zone 0 loopzone

    <out:
      zon 0 rate=0.5 lin=1 and=255

`rate` range is 0..1 (must be const)
`lin` enables bilinear filtering (def=off) (must be const)
`and` enables power-of-two wrap-around (def=off, 1..65535 otherwise) (must be const)
`andf` logic-and read position with float converted to fixpoint (can be dynamic)

| in order to create chords, synthesize the highest note to a zone, then in a following zone read and mix the previously rendered zone at different speeds / rates

Minor chord example:

    zone 0 singlenote_g3

    <out:
      zon 0
      + zon 0 d#3/g-3
      + zon 0 c-3/g-3
      * 0.5
      clp

## 13.58 zsq

Sequence (other) zone sample.
Can be used to create (multi-channel) drum loops. Example:

    zone 0 bd
    zone 1 sd

    <out:
      #       0   1   2   3   4   5   6   7
      = zsq 0 xx......x....x.. note=16 bpm=125 rate=1
      + zsq 1 ....x.......x... bpm=125 rate=1.1 lin=1
      clp

`note` range is 1..256 (step note duration) (must be const)
`bpm` range is 0..1000 (<10: use project bpm) (must be const)
`swing` range is -0.5..0.5 (must be const)
`rate` range is 0.125..8 (must be const)
`lin` range is 0..1 (>=0.5: enable bilinear filtering, def=0) (must be const)
`div` range is 0..256 (divide source zone into n regions) (must be const)
`off` range is 0..1 (select sub-divided source region, e.g. `$wt_x`)

| When the input zones (one-shot samples) are placed in the same samplebank as the sequenced loop, ticking the `Recalc All` checkbox (next to the `F11: Synth` button at the bottom of the screen) will cause all procedural zones to be recalculated when any synth patch changes. This makes it possible to tweak the input sounds and immediately hear the edits in the context of the loop.

| `rate` can (as usual) also be expressed as a note ratio, e.g. `c-3/d#3`

# 14 Plugins

Cycle patches can also be exported as native code (C/C++) voice plugins.
The following restrictions apply:

- no references to sample zones (e.g. `zon`, `zlp`, `zsq`, `slf`, `ssl`)
- no references to voice plugins (`vpl`)
- `dur`, `wt_w`, `wt_h`, `wt_cyclelen`, `wt_freqs`, `skip`, `xfade`, `mirror`, `zone` keywords are not allowed

## 14.1 Plugin header

Patches must declare a unique plugin id. Example:

    id     myplugin_v1
    name   "my plugin v1"
    author bsp
    cat    osc

## 14.2 Parameters and modulations

Plugins have "n" parameters (shared among all voices), and "n" mods (per-voice modulation).
It is recommended to not exceed 8 parameters and mods.

| parameter names may be prefixed with `p_` (prefix will be removed during export)

| mod names may be prefixed with `m_` (prefix will be removed during export)

If a mod has a parameter of the same name (ignoring the optional prefix), the effective mod value is the sum of the shared and mod values.

    float modcutoff = shared->params[PARAM_CUTOFF] + voice->mods[MOD_CUTOFF];

auto-generated code example
Frequency, parameters, and modulation will be interpolated over processing blocks. The block update rate (in Eureka) is 1000Hz, i.e. a processing block consists of e.g. 48 sample frames at a sample rate of 48kHz.

### 14.2.1 Params

Parameters are always in the (normalized) range `0..1`.
Parameter may have default (reset) values, which will be exported to the plugin:

    param p_cutoff 0.9

Normalized parameters are scaled to their (optional) min / max value range when they are accessed.

    param p 0.5 1 16

p will be scaled to range 1..16. the effective default value is 0.5*(16-1)+1=8.5

#### 14.2.1.1 Param Preset Values

Each param can be assigned a list of preset values (shown when hold-clicking the param value widget).
Example:

    param p_angle 0 0 1

    param_values angle 0="0 deg" 0.25="90 deg" 0.5="180 deg" 0.75="270" 1="360 deg"

| parameter preset values always refer to unscaled, normalize 0..1 values

### 14.2.2 Mods

Mods are (usually) in the range `-1..1`.
Mods have an implicit (reset) value of 0.

    mod m_cutoff

### 14.2.3 Args and envelopes

(Regular) args and curves (spline envelopes) will be baked into the plugin source.

### 14.2.4 Audio input

The initial value is the output of the previous sound module (voice plugin, sample, VST effect, ..).

    id     my_amp
    name   "my amp"
    author bsp
    cat    amp

    param p_level 1
    mod   m_level

    <out:
      * $m_level

simple "amp" plugin

### 14.2.5 init

The `<init:` section can be used to implement macro parameters that calculate additional internal (voice state) variables from user defined parameters. Code placed in this section will be executed everytime a new note is played.

### 14.2.6 prepare

The `<prepare:` section is evaluated before the main section(s), and after the (optional) `<init:` section.
Heavy calculations, or calculations which do not need to be evaluated per sample frame should be placed in the `prepare` section.
Code placed in this section will be executed once per processed block. This can be used to implement macro modulation that reads the current modulation variables and writes one or many (internal voice-state) variables.

### 14.2.7 Free runnning oscillators

By default, the phase and noise generator states reset when a new note is played.
The `sin`, `tri`, `saw`, `pul`, `pha`, `fsr`, `nos` modules have a (constant) `reset` input that determines whether the reset occurs at note on (`reset=1`, default), or just once when the voice is allocated (`reset=0`). The `cycle` input should be set to 0 (`cycle=0`).
Example:

    <out:
      fsr cycle=0 reset=0

free-running linear feedback shift register noise oscillator

## 14.3 Export

Press `lctrl-p` in the Cycle dialog to export the current patch as a plugin (and (re)-load it).
Press `lctrl-lshift-p` to toggle auto-export when patch is saved / edited (`lctrl-s` or after arg changes).

## 14.4 Compiler installation

Cycle uses Clang / GCC to compile the plugins.
No knowledge of C/C++ is required to create new plugins but the following (free) build tools have to be installed so the application can invoke them in the background.

### 14.4.1 Windows

Please install MSYS2 https://www.msys2.org/, then (in MSYS2 bash console) type:

    $ pacman -Syu
    $ pacman -S base-devel gcc

to update the package database and install the compiler environment.

| In case MSYS2 was not installed to its default directory, edit `eureka_config.tks` and update the `stfx_cc_msys2_dir` variable accordingly.

### 14.4.2 macOS

Please install brew https://brew.sh/ (which will also install the XCode commandline tools):

    $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

(see https://mac.install.guide/commandlinetools/3.html)

### 14.4.3 Linux

On Debian or Ubuntu, type

    $ sudo apt update
    $ sudo apt install build-essential

Document created on 16-Dec-2023 12:41:16