% Cycle Sample Generator / Soft Synth
% bsp
% 16-Dec-2023
>>>
11Oct2020, 13Oct2020, 14Oct2020, 31Jul2021, 12Aug2021, 24Aug2021, 16Jan2022, 06Jan2023, 07Jan2023, 28Mar2023, 07Apr2023, 19Apr2023, 04May2023, 05May2023, 06May2023, 07May2023, 08May2023, 09May2023, 12May2023, 16May2023, 27May2023, 28May2023, 29May2023, 31May2023,
01Jun2023, 05Aug2023, 19Aug2023, 23Nov2023, 24Nov2023, 30Nov2023, 15Dec2023
<<<
$(var:header)


# Table of Contents
$(buf!toc)


# Preface

`Cycle` is a modular soft-synth which emits `TKS` or `C` sample generation code, or [STFX](https://github.com/bsp2/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.



# Global Keywords

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

>>>note_os
note: 8 is usually a good factor when targeting ~16kHz samples (=128kHz internal rate)
<<<

>>>caution_os
caution: Oversampling is ignored when exporting to lo-fi 8bit "C" sample generator source
<<<

>>>note_wtfreq
note: the current cycle freq is available via `$wt_freq` variable during synthesis
<<<

| Keyword            :|: Description
| arg                :| Declare argument (will be editable in UI)
| var                :| Declare variable
| oversample_factor  :| Oversample factor (0.03125..32, def=1) (also supports fractional values, e.g. "1/8")
|+oversample          |+ $(buf!caution_os)
|                    :|+ $(buf!note_os)
| oversample_quality :| Oversample quality (0..10, def=4)
|                     |+ $(buf!caution_os)
| 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`
|                     |+ $(buf!note_wtfreq)
| 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][].
].cmdline Global Keywords

>>>plugin_name
note: the plugin name (and dll / so / dylib) will be prefixed with `cycle_`
<<<

>>>plugin_cat
|noheader
| |:unknown     |:limiter  |:pan     |:transient
| | ringmod     | enhance  | pitch   | convolver
| | waveshaper  | dac      | chorus  | mixer
| | filter      | lofi     | flanger | comparator
| | eq          | gate     | delay   | utility
| | compressor  | amp      | reverb  | osc
].table_noborder_small
<<<

## Plugin keywords
Keywords used in plugin-generation mode:
| Keyword            :|: Description
| name               :| Plugin name
|+                    |+$(buf!plugin_name)
| author             :| Plugin author
| cat                :|
|+category            |+Plugin category
|+                    |+$(buf!plugin_cat)
].cmdline Plugin Keywords



# 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.



# 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



# 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

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

note: the default table size is 64
note: the default table mode is "voice"
note: LUT init sections must be named "lut_\<id\>"
note: if no init section has been declared for a LUT used by a "lut" module, compilation will fail
note: the shared LUTs are calculated after the "\<init\:" section
note: the per-voice (or per-WT cycle) LUTs are updated after the "\<prepare\:" / "\<wt\_init\:" section

## Inverse Lookup Tables
See [#sta][] module documentation for examples.


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

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

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


# Output lanes

Lanes divide the script into multiple output sections.

Each script **must** contain the main section (named `out`):
~~~
<out:
  sin
~~~

## 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).


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

note: the `init` section must occur before the first audio lane

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


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

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


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

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


# 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
~~~


# 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
~~~

caution: args and variables share the same namespace

## 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"
~~~~

note: arg preset values always refer to _scaled_ values, i.e. within min..max range

# 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
~~~

caution: args and variables share the same namespace

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


# 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`.



# 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.



## abs
] discard sign of previous output

Return absolute value of previous output.

Example:
~~~
<out:
  -42
  abs
~~~


## bit
] bit-crush previous output

Bit-crush (shift) previous output.

Example:
~~~
<out:
  sin
  bit shift=0.5
  bit 0.5
~~~

`shift` range is 0..1


## boo
] boost previous output

Boost sample deltas of previous output.

Example:
~~~
<out:
  sin
  boo amount=0.6
  boo 0.6
~~~

`amount` range is 0..1


## box
] box filter previous output

Box filter previous output.

Example:
~~~
<out:
  saw
  box freq=0.5
  box 0.5
~~~

`freq` range is 0..1


## bpf

**not implemented, yet**
tip: use [#svf][] in mode=2 instead


## bts
] bipolar to scale, e.g. -1..1=\>1/16..\*1..\*16

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\]

note: uses `div` input when `mul` is not connected

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


## buf

**not implemented, yet**


## bus
] read sample from (cross-zone) voicebus

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)

caution: only supported in STFX plugin mode (returns 0 otherwise)


## clp
] clip previous output

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)


## dec
] decrement variable

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


## div
] divide constant values

Divide constant values.

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

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


## dly

**not implemented, yet**


## drv

**not implemented, yet**


## end
Close current lane.

note: optional. can be used for placing [#lut][] declarations in between lanes.

note: when lane is a lut init lane, skip the output write (lane must write to lut via [#sta][] instead)


## env

**not implemented, yet, may be removed**
(use [#rmp][] and [#lut][] instead)


## fld
] fold previous output

Fold previous output value at ceiling.

Example:
~~~
<out:
  sin
  * 2.0
  fld ceil=1.0
~~~
note: `ceil` sets min=-ceil, max=+ceil


Example:
~~~
<out:
  sin
  * 2.0
  fld min=-1.0 max=1.0
~~~
note: `min` / `max` override default ceil

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


## fma
] fused multiply add

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\]


## frc
] fractional part of previous output

Calc fractional part of previous output (discard integer bits)

Example:
~~~
<out:
  sin
  * 3.3
  frc
~~~

note: [#int][] does the opposite and discards all fractional bits


## fsr
] linear feedback shift register noise

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)

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

note: `fsr` noise contains less low frequencies than `nos` noise


## hbx
] high-pass box filter

High-pass Box filter previous output.

Example:
~~~
<out:
  saw
  hbx freq=0.5
  hbx 0.5
~~~

`freq` range is 0..1


## hld
] sample and hold previous output

Sample and hold previous output.

Example:
~~~
<out:
  hld rate=0.5
~~~

`hld` range is 0..1

note: 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`


## hpf
] high-pass filter previous output

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

Example:
~~~
<out:
  saw
  hpf cfg=3
~~~


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


## if
] conditional code generation

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


## inc
] increment variable

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


## int
] emit sub-tree in integer mode

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

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


## kbd
] MIDI note keyboard tracking

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) [def=4]
`clamp` toggles clamping to 1.0 (def=true) (should be set to 0 when using this for pitch tracking) (must be const)

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


## lle
] log / lin / exp scaling

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.

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


## log
] calc logarithm of previous output

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.

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


## lop
] loop statement sequence

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

CAUTION: 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.
TIP: use `rep` to generate unrolled loops where each iteration will have its own state


## lpf
] 1RC low pass filter

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


## lut
] read from look-up table

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)

note: the `lut` module is often combined with a [#rmp][] module that generates the look-up index


## mac
] update interpolated macro fields

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)


## neg
] negate previous output

Negate previous output

Example:
~~~
<out:
  saw
  neg
~~~


## nos
] generate xor-shift white noise

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)


## pha
] phase accumulator

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)


## p2s
] power-of-two scaling

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 [7]
`scl` is in the range 0..1 [0] (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)

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

tip: for keyboard tracking purposes, the [#kbd][] module is usually the preferred solution


## pow
] raise previous output to the power of 'f'

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

tip: useful for bending [#rmp][] envelopes into non-linear shapes

caution: constant exponents 2 and 3 behave differently than arbitrary (or dynamic) exponents, i.e. -2²=+4 while e.g. -2^4=-16
caution: arbitrary or dynamic exponents are only supported in float-mode (fall back to float in int mode)


## pul
] generate pulse wave

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)

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


## qua
] quantize previous output

Quantize float value to n fractional steps per integer.

~~~
<out:
  sin
  qua num=10
~~~

`num` is in the range 1..n

tip: useful for controllers, e.g. FM ratios

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

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


## rep
] loop / unroll statement sequence

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



## rev

**not implemented, yet**


## rmp
] generate linear ramp

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)


## saw
] generate sawtooth wave

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)

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


## set
] set variable

Set variable

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


## sin
] generate sine wave

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)

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


## slf
] read past sample output

Read past sample output ("self" feedback)

~~~
<out:
  pul
  + slf rate=0.98
  clp
~~~

`rate` range is 0..1


## ssl
] sine slice oscillator

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)


## sta
] store variable in LUT

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

note: 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


## sto
] store previous output in variable

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


## svf
] state-variable filter (lpf, hpf, bpf, notch)

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


## tanh
] saturate previous output

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

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

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


## trc
] debug-trace last output value

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

Example:
~~~
lut mylut shared 16

<lut_mylut:
  # print current $x
  trc
  
<out:
  sin
~~~


## tri
] generate triangle wave

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)

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


## vpl
] invoke voice plugin

Process previous output via [stfx](https://github.com/bsp2/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.


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

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

caution: `vpl` modules cannot be exported to "C" source


## xfd
] crossfade

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)


## zlp
] read looped sample frame from other zone

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)

note: sample len is determined by last loop length of the specified zone
note: sample len should be a power of two (read offset is ANDed with (sampleLen-1))


## zon
] read sample frame from other zone

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)

tip: 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
~~~


## zsq
] sequence zone sample

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

tip: 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.

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



# 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

## Plugin header
Patches **must** declare a unique plugin id. Example:
~~~
id     myplugin_v1
name   "my plugin v1"
author bsp
cat    osc
~~~

## 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.

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

note: 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.

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

#### 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"
~~~~

note: parameter preset values always refer to _unscaled_, normalize 0..1 values

### Mods
Mods are (usually) in the range `-1..1`.

Mods have an implicit (reset) value of 0.

~~~
mod m_cutoff
~~~

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

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

### 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.

### 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.

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


## 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).

## 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.


### 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.

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

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

### Linux
On Debian or Ubuntu, type
~~~
$ sudo apt update
$ sudo apt install build-essential
~~~



$(buf!footnote)
$(buf!w3validator)

Document created on $(var:localdatetime)
].create
