Sitemap

FL Studio Formula Controller Tutorial and Formulas BONUS PYTHON SCRIPT TO LET YOU DRAW THE FORMULA PLOT AT THE END!!

16 min readDec 16, 2025

--

*********************************** NOTE MEDIUM Changes the “MINUS SIGN WITH EN DASH***************** this will cause formulas with the “-” sign to not compile properly because they may selective be changed to a “en dash” character. So you may need to manually type some formulas. I am working on saving all these formulas as .fst files that can be downloaded as a package and used as presets.

Load the Plugin Add Fruity Formula Controller to a Mixer FX slot (or use in Patcher for chaining). You can place it on any open fx track however if you have room placing it in the same track you are linking it to may help organizationally so you know what formula controllers are linked to what. So you can keep formula controllers on their own mixer tracks or grouped with the effects they modulate. It is your choice where you load it.

Rename it (e.g., “Filter LFO”) to identify it easily in links, give it a descriptive name as to what it is doing as this is how yuo will know what formula is for what if you have multiple formula controllers active in a project. In FL Studio 2025, it may auto-name itself after a formula preset name if one was loaded — give it a unique name to avoid confusion.

Enter a Formula Type or paste into the Formula field/box. Press ENTER or click Compile. If valid, you’ll see “Compiled ok.” Errors like “invalid identifier” usually mean unsupported syntax (e.g., no ternary ?:, no user variables beyond a/b/c).

Control Inputs Link knobs, automation clips, or MIDI to a, b, c (all in 0–1 range). You can automate these knobs to record live tweaks for dynamic modulation.

SongTime-based formulas only animate/plot during playback.

Always clamp output to 0–1 with min(1,max(0,expression)) to prevent modulation overshoot/clipping artifacts.

Link Output Right-click target knob (e.g., filter cutoff) → Link to controller → Select “Formula ctrl — Out” (or your renamed version).

Key Syntax Rules ALWAYS USE a, b, c — NOT x, y, z or custom variables (none allowed beyond these three).

  • Inputs: a, b, c (knobs), SongTime() (quarters), Time() (ms), GetTempo().
  • Operators: + − * / ^ (power), mod (modulo), div (integer divide), = <> > >= < <= (comparisons return 1=true, 0=false), and/or/xor/not (logical), shr/shl (bit shifts).
  • Functions (non-exhaustive): sin/cos/tan, abs/floor/ceil/sqrt/pow, min/max, frac (fractional part), int (integral part), round, rand/seededrand (random), fold/warp (wave shaping), if (conditional: if(a>0.5,b,c)), map (remap values), ArcCos, and more. See full list in plugin help (question mark icon).
  • Constants: Pi.
  • Not Allowed: Loops, user-defined vars beyond a/b/c, ternary ?:, % (use mod instead). Syntax is case-insensitive.

Logic Trick No full if/else blocks, but use comparisons as multipliers: X*(condition) + Y*(1-condition) for branching (or use built-in if).

Tempo Sync Multiply SongTime() by Pi*2 for even full cycles. Use GetTempo() for BPM-aware rates.

Best Practices for Music Production

EDM/Electronic: Risers (sawtooth), pumping sidechains (pulse), glitch (rand), rhythmic gates (mod/floor).

  • Top 40 Polish: Subtle modulation on pads/vocals/macros — gentle sines on reverb, humanized curves on velocity.
  • Clamping: Essential for safe macros.
  • Testing: Play project; tweak a/b/c live.
  • Chaining: Route multiple Formula Controllers in Patcher for complex systems (e.g., LFO modulating LFO).
  • Prompt for AI (e.g., Grok/ChatGPT): “Create a single-line FL Studio Fruity Formula Controller expression using only a, b, c, SongTime, Pi, and supported functions like sin, mod, min, max. Clamp to 0–1. No ternary or extra vars. For [e.g., EDM sawtooth riser].”

Working Formula Examples

All are single-line, copy-paste ready. Grouped by type with explanations, use cases, and a/b/c roles. All include clamping where needed.

1. Basic Curves (Mapping & Response Shaping) Great for macro knobs in Top 40 — one knob controls multiple params musically.

  • Linear Passthrough (default, but clamped): min(1,max(0,a)) What it does: Direct input. Use: Safe macro base. a = input value. Music: EDM filter macro start.
  • Exponential Curve (snappy response): min(1,max(0,pow(a,3))) What it does: Slow start, fast end. Use: Velocity compression in pop keys/synths. a = input.
  • Log-Like Curve (gentle, musical): min(1,max(0,sqrt(a))) What it does: Expands lows, compresses highs. Use: Humanized fader response. a = input.
  • S-Curve (smooth transitions): min(1,max(0,a*a*(3–2*a))) What it does: Sigmoid shape. Use: Filter macros or crossfades in electronic builds.

2. LFO Shapes (No Extra Plugins Needed) Sync to tempo with SongTime()*Pi*2*c for even cycles. Ideal for EDM wobbles or pads.

  • Sine LFO (smooth modulation): min(1,max(0,0.5+0.5*sin(SongTime()*Pi*2*c+(b*Pi)))) What it does: Oscillating wave. a: Amplitude (depth). b: Phase offset. c: Speed (freq). Use: Filter cutoff in house/EDM; pan for stereo in Top 40.
  • Triangle LFO (linear ramps): min(1,max(0,0.5+0.5*ArcCos(cos(SongTime()*Pi*2*c+(b*Pi))))) What it does: Sharp peaks. a: Amplitude. b: Phase. c: Speed. Use: Bass modulation in dubstep; subtle vibrato in synth pop.
  • Sawtooth LFO (risers/falls): min(1,max(0,0.5+a*frac(SongTime()*c))) What it does: Linear ramp/reset. a: Amplitude. b: Skew (use in advanced variants). c: Speed. Use: EDM build-ups; sweeps.
  • Square/Pulse LFO (on/off pulsing): min(1,max(0,a*round(0.5+0.5*sin(SongTime()*Pi*2*c))+b)) What it does: 50% duty cycle square. a: Amplitude. b: Offset. c: Speed. Use: Stutter in trap/EDM; sidechain gates in Top 40.

3. Rhythmic Gates & Triggers (Beat-Synced Groove) Use mod or floor for patterns.

  • On/Off Gate (simple trigger): min(1,max(0,(SongTime()*c mod 2)>0.5)) What it does: Toggles based on threshold. a/b: Unused (link for depth). c: Rate. Use: Trance gates on pads.
  • Trance Gate (8-Step): min(1,max(0,floor(SongTime()*c*8) mod 2)) What it does: Alternating steps. a/b: Depth/offset. c: Speed (scale with GetTempo/240 for BPM sync). Use: Classic gating on synths.
  • Accent Every 4th Beat: min(1,max(0,(floor(SongTime()*c*16) mod 4)=0)) What it does: Pulses on beats. a/b: Amplitude/phase. c: Rate. Use: Drum accents or sidechain triggers.

4. Envelopes & Chaos (Dynamic Shaping) For plucks, risers, randomized elements.

  • Pluck Envelope (fast attack/decay): min(1,max(0,pow(1-a,4))) What it does: Exponential decay. a: Time/progress. b/c: Unused (or scale depth). Use: Plucks; vocal chops.
  • Pseudo-Random Chaos (deterministic jitter): min(1,max(0,a+(sin(SongTime()*50*c)*b*0.05))) What it does: Adds wobble. a: Base. b: Jitter depth. c: Chaos speed. Use: Glitchy effects; humanized modulation.
  • Sample & Hold (Steppy Random): min(1,max(0,b*seededrand(int(SongTime()*a))+c)) What it does: Quantized random steps. a: Step rate. b: Amplitude. c: Offset. Use: Randomized params.

5. Advanced Production Tricks

  • Tempo-Synced Saw (BPM-Aware Riser): min(1,max(0,frac(SongTime()*(GetTempo()/60)*b))) What it does: Ramps synced to BPM. a: Amplitude. b: Cycles per bar. c: Offset. Use: Transitions.
  • Sidechain Compressor Driver (Pulse Gate): :min(1,max(0,round(frac(SongTime()*2*b)+0.5-c))) What it does: Adjustable pulse width. a: Amplitude. b: Rate. c: Width. Use: Pumping without compressor.
  • Conditional Macro (If-Then Logic): min(1,max(0,if(a>0.5,b,c))) What it does: Switches based on threshold. a: Condition. b: True output. c: False output. Use: Switch modes; A/B testing.

Genre Specific Formulas

Rock

Rock often features gritty distortion, steady rhythms, and dynamic builds. These formulas emphasize punchy envelopes and subtle grit.

  • Gritty Distortion Curve (for amp/drive modulation): min(1,max(0,pow(a,1.5+(b*0.5))))
  • What it does: Exponential curve with adjustable aggression. a: Input level. b: Grit intensity. c: Unused (or offset). Use: Modulate overdrive on guitars; adds raw edge to riffs.
  • Punchy Envelope (attack/decay for transients): min(1,max(0,if(a<0.5,a*2*(1+b),pow(1-a,2+c))))
  • What it does: Fast attack, variable decay. a: Progress/time. b: Attack sharpness. c: Decay curve. Use: Drum compression; enhances snare/kick punch in rock mixes.
  • Rhythmic Tremolo (vibrato on leads): min(1,max(0,0.5+a*sin(SongTime*Pi*4*c)*(b-0.5)))

min(1,max(0,0.5+a*sin(SongTime()*Pi*4*c)*(b-0.5)))

  • What it does: Medium-speed oscillation. a: Depth. b: Bias. c: Speed (e.g., 4 for quarter-note sync). Use: Guitar or vocal modulation; classic rock shimmer.

Hip-Hop

Hip-hop thrives on heavy low-end, chopped samples, and rhythmic gates. Formulas focus on bass emphasis and beat-sync.

  • Low-End Boost Curve (for 808/sub modulation): min(1,max(0,sqrt(a)+(b*0.2*abs(sin(SongTime*Pi*2*c)))))
  • min(1,max(0,sqrt(a)+(b*0.2*abs(sin(SongTime()*Pi*2*c)))))
  • What it does: Gentle curve with subtle pulse. a: Base level. b: Pulse depth. c: Rate. Use: EQ low-shelf; fattens kicks and basslines.
  • Chop Gate (stutter/slicing):

min(1,max(0,round(frac(SongTime*8*c)+b-0.5)))

min(1,max(0,round(frac(SongTime()*8*c)+b-0.5)))

  • What it does: On/off gating with adjustable width. a: Amplitude. b: Chop offset. c: Speed (8 for 8th notes). Use: Sample chopping; creates glitchy vocal effects.
  • Velocity Compressor (dynamic control): min(1,max(0,min(1,a*1.2+(b*0.1))))

What it does: Compresses highs, boosts mids. a: Input velocity. b: Makeup gain. c: Unused. Use: Drum programming; evens out hi-hats and snares.

New Age

New Age music is ambient and evolving, with slow, ethereal modulations. These create drifting pads and gentle waves.

  • Slow Drift LFO (for pad evolution): min(1,max(0,0.5+0.3*sin(SongTime*Pi*0.1*c+(b*Pi))))

min(1,max(0,0.5+0.3*sin(SongTime()*Pi*0.1*c+(b*Pi))))

  • What it does: Ultra-slow sine. a: Depth (low values). b: Phase. c: Speed (0.1 for minutes-long cycles). Use: Reverb size or filter; builds atmospheric textures.
  • Ethereal Envelope (rise/fall): min(1,max(0,pow(a,0.5)+(b*0.1*sin(SongTime*Pi*0.05*c))))

min(1,max(0,pow(a,0.5)+(b*0.1*sin(SongTime()*Pi*0.05*c))))

  • What it does: Gentle swell with micro-mod. a: Progress. b: Swell amount. c: Micro-rate. Use: Synth sustains; softens transitions in ambient layers.
  • Harmonic Chaos (subtle randomness):

min(1,max(0,a+seededrand(int(SongTime*0.05*c))*b*0.05))

min(1,max(0,a+seededrand(int(SongTime()*0.05*c))*b*0.05))

  • What it does: Slow-stepped random jitter. a: Base. b: Chaos depth. c: Step rate. Use: Pitch detune; adds organic feel to chimes or flutes.

New Wave

New Wave draws from 80s synth-pop with bright, wavy synths and punchy rhythms. Formulas mimic retro waves and gates.

  • Retro Synth Wave (oscillating filter): min(1,max(0,0.5+a*arccos(cos(SongTime*Pi*2*c))))

min(1,max(0,0.5+a*ArcCos(cos(SongTime()*Pi*2*c))))

  • What it does: Triangle-like wave. a: Depth. b: Offset. c: Speed. Use: Synth cutoff; evokes 80s leads.
  • Punchy Gate (rhythmic muting): min(1,max(0,(floor(SongTime*4*c) mod 2)*a+b))What it does: Half-bar toggle. a: Gate height. b: Base level. c: Rate. Use: Basslines; creates syncopated grooves.
  • Bright Curve (EQ shaping): min(1,max(0,pow(a,0.8)+(b*0.15)))What it does: Compresses lows, boosts highs. a: Input. b: Brightness. c: Unused. Use: Vocal processing; adds sheen to synth-pop hooks.

House Music

House relies on steady grooves, pumping compression, and builds. These provide sidechain and filter sweeps.

  • Pumping Sidechain (volume ducking): min(1,max(0,1-abs(sin(SongTime*Pi*2*c))*a*b))What it does: Rhythmic dip. a: Depth. b: Curve. c: Beat rate (0.25 for quarter notes). Use: Bass vs. kick; classic four-on-the-floor pump.
  • Filter Sweep LFO (build-up modulation): min(1,max(0,frac(SongTime*c)*a+b))

min(1,max(0,frac(SongTime()*c)*a+b))

  • What it does: Slow ramp. a: Range. b: Start point. c: Speed. Use: Risers; opens filters over bars.
  • Groove Jitter (humanize swing): min(1,max(0,a+(sin(SongTime*Pi*8*c)*b*0.02)))

min(1,max(0,a+(sin(SongTime()*Pi*8*c)*b*0.02)))

  • What it does: Fast micro-wobble. a: Base. b: Swing amount. c: Rate. Use: Hi-hats; adds subtle groove.

Trance

Trance features uplifting sweeps, gates, and long builds. Formulas emphasize asymmetry and euphoria.

  • Uplifting Saw Sweep (filter rise): min(1,max(0,0.5+a*frac(SongTime*c*0.5)))

min(1,max(0,0.5+a*frac(SongTime()*c*0.5)))

  • What it does: Slow ascending ramp. a: Depth. b: Offset. c: Cycle length. Use: Build-ups; classic trance tension.
  • Trance Gate (rhythmic chopping): min(1,max(0,(floor(SongTime*16*c) mod 8)<4)*a+b)

min(1,max(0,(floor(SongTime()*16*c) mod 8)

  • What it does: 16th-note pattern. a: Gate depth. b: Base. c: Speed. Use: Pads/synths; hypnotic rhythms.
  • Euphoric LFO (vibrato swell): min(1,max(0,0.5+sin(SongTime*Pi*0.5*c)*a*(b+0.5)))

min(1,max(0,0.5+sin(SongTime()*Pi*0.5*c)*a*(b+0.5)))

  • What it does: Slow, deepening sine. a: Max depth. b: Build rate. c: Base speed. Use: Leads; emotional peaks.

Techno

Techno is minimal, driving, and repetitive. These create steady pulses and industrial mod.

  • Driving Pulse (steady modulation): min(1,max(0,round(0.5+0.5*sin(SongTime*Pi*2*c))*a+b))

min(1,max(0,round(0.5+0.5*sin(SongTime()*Pi*2*c))*a+b))

  • What it does: Square wave beat. a: Depth. b: Offset. c: Tempo fraction. Use: Kick drive; relentless groove.
  • Industrial Jitter (noise mod): min(1,max(0,a+rand*b*0.1))

min(1,max(0,a+rand*b*0.1))

  • What it does: Continuous random. a: Base. b: Noise amount. c: Unused. Use: Distortion; gritty textures.
  • Minimal Envelope (sharp decay): min(1,max(0,if(a<0.2,a*5,pow(1-a,3+c))))What it does: Quick attack, long tail. a: Progress. b: Unused. c: Tail curve. Use: Percussion; tight hits.

Trap

Trap highlights heavy 808s, hi-hat rolls, and dark vibes. Formulas boost subs and rapid patterns.

  • 808 Sub Curve (bass emphasis): min(1,max(0,pow(a,2)+(b*abs(sin(SongTime*Pi*c)))))

min(1,max(0,pow(a,2)+(b*abs(sin(SongTime()*Pi*c)))))

  • What it does: Exponential low-end with pulse. a: Level. b: Pulse depth. c: Slow rate. Use: Bass EQ; deep, rumbling drops.
  • Hi-Hat Roll Gate (rapid stutter): min(1,max(0,(floor(SongTime*32*c) mod 4)=0)*a+b)

min(1,max(0,(floor(SongTime()*32*c) mod 4)=0)*a+b)

What it does:

  • Fast accents. a: Height. b: Base. c: Speed. Use: Hats; triplet rolls.
  • Dark Compression (squash dynamics): min(1,max(0,min(1,a*1.5-b*0.2)))

min(1,max(0,min(1,a*1.5-b*0.2)))

  • What it does: Hard limit with dip. a: Input. b: Threshold. c: Unused. Use: Full mix; aggressive glue.

Drum and Bass

DnB is fast-paced with breaks, bass wobbles, and chaos. These provide high-speed mod and jitter.

  • Breakbeat Gate (chopped rhythms): min(1,max(0,(floor(SongTime*16*c) mod 16)<8)*a+b)
  • What it does: Complex 16th patterns. a: Depth. b: Offset. c: Double-time rate. Use: Drums; amen-style breaks.
  • Bass Wobble LFO (fast oscillation): min(1,max(0,0.5+a*abs(sin(SongTime*Pi*8*c))))

min(1,max(0,0.5+a*abs(sin(SongTime()*Pi*8*c))))

  • What it does: Pulsing rect sine. a: Depth. b: Offset. c: Wobble speed. Use: Reese bass; neurofunk growls.
  • Chaotic Jitter (random steps): min(1,max(0,a+seededrand(int(SongTime*4*c))*b*0.1))What it does: Stepped random. a: Base. b: Intensity. c: Step rate. Use: Effects; unpredictable energy.

Tips

  • Always clamp if unsure: min(1,max(0,Formula))
  • Chain multiple Formula Controllers in Patcher for poly-rhythmic or sidechain-like effects.
  • Use SeededRand() or Frac() for randomized gates/glitch.
  • Adjust a/b/c manually or via automation for dynamic changes

So now you want to automate the functions to Automation clips in fl studio…

Link Formula Controller to a Parameter

Open your target plugin or control (e.g., filter cutoff on Sytrus, mixer volume fader, or reverb wet/dry).

Right-click the control and select:
Link to controller…

In the Internal Controller list, find your Formula Controller output:

Usually listed as Formula ctrl - Out but it may be the preset name or if you renamed your controller, it will show your custom name.

Press Accept.

Use A, B, C Knobs or Automation Clips

Knobs A, B, C in the Formula Controller can be manual controllers for live tweaking pr, you can link them to other automation clips:

  1. Right-click the knob → Link to controller…
  2. Choose any automation clip, MIDI controller, or internal source.
  • This allows dynamic modulation that changes over the song.

Use Formula Controller in Patcher (Advanced)

  • Drag the Formula Controller into a Patcher graph.
  • Link its output to other plugin parameters inside Patcher for complex modulation chains.
  • Combine multiple Formula Controllers for poly-rhythmic or sidechain-like effects.

Example: Driving a Filter Cutoff

Let’s say you want a rhythmic EDM filter sweep:

Formula (Triangle LFO):

0.3*ArcCos(Cos(SongTime*Pi*2*B))+0.1
  • B = speed knob (0–1)
  • Output is clamped between 0–1 and mapped directly to the filter cutoff.
  • Optional: Automate B over the track to gradually speed up the sweep.

Example: Volume Sidechain Without a Compressor

Formula (Square wave / pulse):

Round(0.5*Sin(SongTime*Pi*2*B)+0.5)
  • Output toggles between 0 and 1 → perfect for pumping a bass or pad line.
  • Map the output directly to mixer volume fader.

Tips

  • Always Clamp output if necessary:
min(1,max(0,Formula))
  • Combine multiple Formula Controllers for complex envelopes.
  • Use SeededRand() or Frac() for randomized gates or glitch effects.
  • Adjust A/B/C manually or via automation clips for dynamic changes.

Other resources:

FL Studio: Understanding Mapping Formulas | UnaspectedStudios

BONUS PYTHON SCRIPT!!

import tkinter as tk

from tkinter import filedialog, ttk

import matplotlib.pyplot as plt

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk

import numpy as np

import pyperclip

from scipy.optimize import curve_fit

from scipy.interpolate import CubicSpline

# — — FL Studio Function Definitions for Curve Fitting — -

# NEW: Fourier Series (3 Harmonics)

# Parameters: O (Offset), F (Fundamental Frequency),

# A1, P1 (Amp/Phase of 1st Harmonic), A2, P2 (2nd Harmonic), A3, P3 (3rd Harmonic)

def fourier_func(t, O, F, A1, P1, A2, P2, A3, P3):

# Clamps F to prevent singularities/instability

F_clamped = np.clip(F, 0.01, 10.0)

y = O

y += A1 * np.sin(t * F_clamped * np.pi * 2 + P1) # 1st Harmonic

y += A2 * np.sin(t * F_clamped * np.pi * 4 + P2) # 2nd Harmonic

y += A3 * np.sin(t * F_clamped * np.pi * 6 + P3) # 3rd Harmonic

return y

def calculate_mse(y_true, y_pred):

return np.mean((y_true — y_pred)**2)

# — — DraggablePoints Class (Unchanged from previous revision) — -

class DraggablePoints:

def __init__(self, ax, master_app, max_points=100):

self.ax = ax

self.master = master_app

self.max_points = max_points

self.snap_steps = 128

self.points = [(0.0, 0.0), (1.0, 1.0)]

self.points.sort(key=lambda p: p[0])

self.line, = ax.plot([], [], ‘b-’, linewidth=2)

self.markers = ax.scatter([], [], c=’r’, s=50, picker=5)

self.cid_press = ax.figure.canvas.mpl_connect(‘button_press_event’, self.on_press)

self.cid_release = ax.figure.canvas.mpl_connect(‘button_release_event’, self.on_release)

self.cid_motion = ax.figure.canvas.mpl_connect(‘motion_notify_event’, self.on_motion)

self.cid_pick = ax.figure.canvas.mpl_connect(‘pick_event’, self.on_pick)

self.dragging = None

self.drawing = False

self.update_plot()

def _snap_y(self, y_raw):

y_clamped = np.clip(y_raw, 0, 1)

y_snapped_index = round(y_clamped * (self.snap_steps — 1))

return y_snapped_index / (self.snap_steps — 1)

def on_press(self, event):

if event.inaxes != self.ax or event.button != 1: return

if self.dragging is None:

self.drawing = True

self.points = []

x, y = event.xdata, event.ydata

if x is not None and y is not None:

self.points.append((np.clip(x, 0, 1), self._snap_y(y)))

def on_pick(self, event):

if event.artist != self.markers: return

ind = event.ind[0]

if ind is not None and len(self.points) > ind:

self.dragging = ind

self.drawing = False

def on_release(self, event):

if self.drawing:

self.simplify_points(target_count=30)

if self.dragging is not None or self.drawing:

self.dragging = None

self.drawing = False

self.points.sort(key=lambda p: p[0])

self.update_plot(formula_update=True)

def on_motion(self, event):

x, y = event.xdata, event.ydata

if x is None or y is None or event.inaxes != self.ax: return

if self.dragging is not None:

index = self.dragging

x_clamped = np.clip(x, 0, 1)

y_snapped = self._snap_y(y)

Get William Ashley’s stories in your inbox

Join Medium for free to get updates from this writer.

self.points[index] = (x_clamped, y_snapped)

self.points.sort(key=lambda p: p[0])

self.update_plot(formula_update=True)

elif self.drawing:

new_x = np.clip(x, 0, 1)

new_y = self._snap_y(y)

if not self.points or (abs(new_x — self.points[-1][0]) > 0.005 or abs(new_y — self.points[-1][1]) > 0.005):

self.points.append((new_x, new_y))

if len(self.points) >= 2:

xs, ys = zip(*self.points)

self.line.set_data(xs, ys)

self.ax.figure.canvas.draw_idle()

def simplify_points(self, target_count=30):

if not self.points: return

xs_interp = np.array([p[0] for p in self.points])

ys_interp = np.array([p[1] for p in self.points])

if xs_interp.size < 2: return

uniform_x = np.linspace(xs_interp.min(), xs_interp.max(), target_count)

ys_simplified = np.interp(uniform_x, xs_interp, ys_interp)

new_points = []

for x, y in zip(uniform_x, ys_simplified):

new_points.append((np.clip(x, 0, 1), self._snap_y(y)))

new_points_dict = {f”{x:.4f}”: v for x, v in new_points}

self.points = sorted([(float(k), v) for k, v in new_points_dict.items()], key=lambda p: p[0])

self.update_plot(formula_update=True)

self.master.update_status(“Curve points simplified for cleaner formula generation.”)

def smooth_points(self):

if len(self.points) < 4:

self.master.update_status(“Need at least 4 points to smooth.”)

return

xs = np.array([p[0] for p in self.points])

ys = np.array([p[1] for p in self.points])

cs = CubicSpline(xs, ys)

new_xs = np.linspace(xs.min(), xs.max(), 50)

new_ys = cs(new_xs)

new_points = []

for x, y in zip(new_xs, new_ys):

new_points.append((np.clip(x, 0, 1), self._snap_y(y)))

self.points = sorted(list(set(new_points)), key=lambda p: p[0])

self.update_plot(formula_update=True)

self.master.update_status(“Curve smoothed and simplified.”)

def update_plot(self, formula_update=False):

if len(self.points) < 2:

self.line.set_data([], [])

self.markers.set_offsets([])

else:

xs, ys = zip(*self.points)

self.line.set_data(xs, ys)

self.markers.set_offsets(np.c_[xs, ys])

self.ax.figure.canvas.draw_idle()

if formula_update:

self.master.update_formula()

def get_points_normalized(self):

return sorted(self.points, key=lambda p: p[0])

# — — Formula Generation Function (FIXED P2/P3 in Dynamic Formula) — -

def get_formula_outputs(points, input_var):

“””

Tries Fourier Series fit.

“””

if len(points) < 5:

return “Not enough points for Best Fit”, “Not enough points for Best Fit”

xs = np.array([p[0] for p in points])

ys = np.array([p[1] for p in points])

mean_y = ys.mean()

crossings = np.where(np.diff(np.sign(ys — mean_y)))[0]

F_guess = max(0.5, len(crossings) / 2)

A_guess = (ys.max() — ys.min()) / 2.0

O_guess = (ys.max() + ys.min()) / 2.0

# 1. Fourier Series Fit (3 Harmonics)

try:

# Initial guesses: O, F, A1, P1, A2, P2, A3, P3

p0_fourier = [O_guess, F_guess, A_guess, 0.0, 0.0, 0.0, 0.0, 0.0]

# Set bounds to keep the fit reasonable

bounds = (

[-1.0, 0.01, -1.0, -2 * np.pi, -1.0, -2 * np.pi, -1.0, -2 * np.pi],

[2.0, 10.0, 1.0, 2 * np.pi, 1.0, 2 * np.pi, 1.0, 2 * np.pi]

)

popt_fourier, _ = curve_fit(fourier_func, xs, ys, p0=p0_fourier, bounds=bounds, maxfev=5000)

y_pred_fourier = fourier_func(xs, *popt_fourier)

mse_fourier = calculate_mse(ys, y_pred_fourier)

O, F, A1, P1, A2, P2, A3, P3 = popt_fourier

F = np.clip(F, 0.01, 10.0)

# — — 1a. EXACT FORMULA (All Constants) — -

exact_terms = []

if abs(A1) > 0.001: exact_terms.append(f”{A1:.4f} * Sin({input_var} * {F:.4f} * Pi * 2 + {P1:.4f})”)

if abs(A2) > 0.001: exact_terms.append(f”{A2:.4f} * Sin({input_var} * {F:.4f} * Pi * 4 + {P2:.4f})”)

if abs(A3) > 0.001: exact_terms.append(f”{A3:.4f} * Sin({input_var} * {F:.4f} * Pi * 6 + {P3:.4f})”)

fourier_exact = f”Min(1, Max(0, {O:.4f} + {‘ + ‘.join(exact_terms)}))”

# — — 1b. DYNAMIC FORMULA (A/B/C) — -

dynamic_terms = []

# Fundamental (1st Harmonic): Amplitude * a, Phase + b * Pi, Frequency * c

if abs(A1) > 0.001:

dynamic_terms.append(f”{A1:.4f} * a * Sin({input_var} * {F:.4f} * c * Pi * 2 + b * Pi)”)

# Higher Harmonics: Amplitude * a, Phase uses CONSTANT Pn value, Frequency * c

# FIXED: Use {P2:.4f} and {P3:.4f} (numerical constants) instead of P2 and P3 (invalid variables)

if abs(A2) > 0.001:

dynamic_terms.append(f”{A2:.4f} * a * Sin({input_var} * {F:.4f} * c * Pi * 4 + {P2:.4f})”)

if abs(A3) > 0.001:

dynamic_terms.append(f”{A3:.4f} * a * Sin({input_var} * {F:.4f} * c * Pi * 6 + {P3:.4f})”)

fourier_dynamic = f”Min(1, Max(0, {O:.4f} + {‘ + ‘.join(dynamic_terms)}))”

if mse_fourier > 0.01:

return (

f”No close harmonic fit found (MSE: {mse_fourier:.4f}). Use Exact Linear Formula.”,

f”No close harmonic fit found (MSE: {mse_fourier:.4f}).”

)

return fourier_exact, fourier_dynamic

except Exception as e:

return f”Fourier fit failed: {e}”, “Fourier fit failed.”

def generate_piecewise_formula(points, input_var):

“””

Generates the exact piecewise linear formula expression for FL Studio.

“””

if len(points) < 2: return “0”

points = sorted(points, key=lambda p: p[0])

t = input_var

# Ensure there’s a segment to start

if len(points) == 2:

x0, y0 = points[0]; x1, y1 = points[1]

try: slope = (y1 — y0) / (x1 — x0)

except ZeroDivisionError: slope = 0

formula = f”({y0:.4f} + {slope:.4f} * ({t} — {x0:.4f}))”

return f”Min(1, Max(0, {formula}))”

# Start the formula from the last two points

x_n_2, y_n_2 = points[-2]; x_n_1, y_n_1 = points[-1]

try: slope = (y_n_1 — y_n_2) / (x_n_1 — x_n_2)

except ZeroDivisionError: slope = 0

formula = f”({y_n_2:.4f} + {slope:.4f} * ({t} — {x_n_2:.4f}))”

# Build the nested If statements backwards

for i in range(len(points) — 2, 0, -1):

x0, y0 = points[i-1]; x1, y1 = points[i]

try: slope = (y1 — y0) / (x1 — x0)

except ZeroDivisionError: slope = 0

segment = f”({y0:.4f} + {slope:.4f} * ({t} — {x0:.4f}))”

formula = f”If({t} < {x1:.4f}, {segment}, {formula})”

return f”Min(1, Max(0, {formula}))”

# — — Tkinter GUI Application (Finalized Layout) — -

class FLFormulaDrawerApp(tk.Tk):

def __init__(self):

super().__init__()

self.title(“FL Formula Drawer (Harmonic LFO Generator)”)

main_frame = ttk.Frame(self, padding=”10")

main_frame.pack(fill=tk.BOTH, expand=True)

# Input Variable Selector (Simplified to only SongTime())

input_select_frame = ttk.Frame(main_frame)

input_select_frame.pack(fill=tk.X, pady=(0, 5))

ttk.Label(input_select_frame, text=”Input Variable (Time Domain): SongTime() (Periodic LFO)”).pack(side=tk.LEFT, padx=(0, 10))

self.input_var = tk.StringVar(value=”SongTime()”)

# Matplotlib Figure and Canvas

fig, self.ax = plt.subplots(figsize=(6, 4))

self.ax.set_xlim(0, 1)

self.ax.set_ylim(0, 1)

self.ax.set_title(“Draw Periodic LFO Shape (Harmonic Fit Applied)”)

self.ax.set_xlabel(“Time (X-Axis 0–1 Bar/Cycle)”)

self.ax.set_ylabel(“Modulation Output (Y-Axis 0–1)”)

self.canvas = FigureCanvasTkAgg(fig, master=main_frame)

self.canvas_widget = self.canvas.get_tk_widget()

self.canvas_widget.pack(fill=tk.BOTH, expand=True)

toolbar = NavigationToolbar2Tk(self.canvas, main_frame)

toolbar.update()

toolbar.pack(side=tk.TOP, fill=tk.X)

self.draggable = DraggablePoints(self.ax, self)

# — — Control Buttons — -

control_frame = ttk.Frame(main_frame)

control_frame.pack(fill=tk.X, pady=(5, 5))

ttk.Button(control_frame, text=”Snap to Contour (Smooth)”, command=self.draggable.smooth_points).pack(side=tk.LEFT, padx=5)

ttk.Button(control_frame, text=”Generate/Copy Formulas”, command=self.handle_copy_and_generate).pack(side=tk.LEFT, padx=5)

self.status_label = ttk.Label(control_frame, text=””)

self.status_label.pack(side=tk.LEFT, padx=10)

# — — Formula Display Area — -

formula_frame = ttk.Frame(main_frame)

formula_frame.pack(fill=tk.X, pady=(5, 0))

# 1a. Fourier Exact Formula

ttk.Label(formula_frame, text=”1a. HARMONIC BEST FIT EXACT (Constant Coefficients — Best Irregular Match)”).pack(anchor=tk.W)

self.exact_formula_text = tk.Text(formula_frame, height=3, wrap=tk.WORD, borderwidth=2, relief=”groove”, background=”#E0F7FA”)

self.exact_formula_text.pack(fill=tk.X, pady=(0, 5))

# 1b. Fourier Dynamic Formula

ttk.Label(formula_frame, text=”1b. HARMONIC BEST FIT DYNAMIC (A/B/C — Live Control) [FIXED SYNTAX]”).pack(anchor=tk.W)

self.dynamic_formula_text = tk.Text(formula_frame, height=3, wrap=tk.WORD, borderwidth=2, relief=”groove”, background=”#FBE9E7")

self.dynamic_formula_text.pack(fill=tk.X, pady=(0, 5))

# 2. Piecewise Linear Formula

ttk.Label(formula_frame, text=”2. EXACT PIECEWISE LINEAR FORMULA (Guaranteed Match for Sharp Edges)”).pack(anchor=tk.W)

self.linear_formula_text = tk.Text(formula_frame, height=4, wrap=tk.WORD, borderwidth=2, relief=”groove”)

self.linear_formula_text.pack(fill=tk.X)

# — — File Buttons Frame — -

file_button_frame = ttk.Frame(main_frame)

file_button_frame.pack(fill=tk.X, pady=5)

ttk.Button(file_button_frame, text=”Save Formulas (.txt)”, command=self.save_formulas).pack(side=tk.LEFT, padx=5)

ttk.Button(file_button_frame, text=”Snapshot Plot (.png)”, command=self.save_plot).pack(side=tk.LEFT, padx=5)

self.update_formula()

def update_status(self, message):

self.status_label.config(text=message)

def handle_copy_and_generate(self):

self.update_formula(live_update=False)

self.copy_exact_formula()

def update_formula(self, live_update=False):

if live_update and (self.draggable.dragging is None and not self.draggable.drawing):

return

points = self.draggable.get_points_normalized()

input_variable = self.input_var.get()

# Get Harmonic Best Fit Formulas

exact_formula, dynamic_formula = get_formula_outputs(points, input_variable)

self.exact_formula_text.delete(1.0, tk.END)

self.exact_formula_text.insert(tk.END, exact_formula)

self.dynamic_formula_text.delete(1.0, tk.END)

self.dynamic_formula_text.insert(tk.END, dynamic_formula)

# Update Piecewise Linear Formula

linear_formula = generate_piecewise_formula(points, input_variable)

self.linear_formula_text.delete(1.0, tk.END)

self.linear_formula_text.insert(tk.END, linear_formula)

self.update_status(“Formulas generated (Harmonic Best Fit copied to clipboard).”)

def copy_exact_formula(self):

formula = self.exact_formula_text.get(1.0, tk.END).strip()

if formula and “No close harmonic fit found” not in formula:

try:

pyperclip.copy(formula)

except Exception as e:

self.update_status(f”Error copying: {e}. Copy manually.”)

elif “No close harmonic fit found” in formula:

self.update_status(“No good harmonic fit found. Copying Piecewise Linear instead.”)

pyperclip.copy(self.linear_formula_text.get(1.0, tk.END).strip())

else:

self.update_status(“No formula generated.”)

def save_formulas(self):

exact_formula = self.exact_formula_text.get(1.0, tk.END).strip()

dynamic_formula = self.dynamic_formula_text.get(1.0, tk.END).strip()

linear_formula = self.linear_formula_text.get(1.0, tk.END).strip()

if not exact_formula and not linear_formula: return

f = filedialog.asksaveasfile(mode=’w’, defaultextension=”.txt”,

filetypes=[(“Text files”, “*.txt”)],

title=”Save FL Formulas”)

if f is None: return

content = (

f” — — Input Variable Used: {self.input_var.get()} — -\n\n”

f”1a. HARMONIC BEST FIT EXACT FORMULA (Use for perfect 1:1 match to drawing)\n”

f”{exact_formula}\n\n”

f”1b. HARMONIC BEST FIT DYNAMIC FORMULA (Use for live control via A, B, C knobs)\n”

f”{dynamic_formula}\n\n”

f”2. EXACT PIECEWISE LINEAR FORMULA (Guaranteed match for complex shapes)\n”

f”{linear_formula}\n”

)

f.write(content)

f.close()

self.update_status(f”Formulas saved to {f.name}”)

def save_plot(self):

f = filedialog.asksaveasfilename(defaultextension=”.png”,

filetypes=[(“PNG files”, “*.png”)],

title=”Save Plot Snapshot”)

if f:

self.ax.figure.savefig(f)

self.update_status(f”Plot snapshot saved to {f}”)

if __name__ == “__main__”:

app = FLFormulaDrawerApp()

app.mainloop()

--

--