Page 1 of 1

Idea for Frequency Domain FX Transfer Function Plugin

Posted: Tue Nov 18, 2025 11:46 am
by ewanpettigrew

I don't think this idea exists in any other music software.

I have been working quite a bit with Xpressive lately, and the possibilities it gives for shaping waves in the time domain are enormous. The fact that anyone with a little bit of time domain knowledge can rapidly create new timbres is amazing.

This got me thinking, if LMMS was able to implement an FX plugin which allowed the user to enter a transfer function in the frequency domain (instead of a time domain formula such as in Xpressive), the possibilities for user defined effects would be greatly expanded. I think this would really set LMMS apart from other music software.

The plugin could perform a Fast Fourier Transform (FFT) on the incoming audio, multiply by the user’s transfer function H(f), and then perform an inverse FFT.

For example, the user could enter something like:

H(f) = 1 / (1 + (f/2000)4)

to create a low-pass filter.

I believe that a transfer-function FX plugin could allow rapid creation of many different audio processes defined entirely by the user, similar to how Xpressive has expanded what is possible using time domain formulas.

In the frequency domain this could include things like:
custom EQ curves, spectral gating, spectral warping, phase shaping, pitch/timbre shifting, noise shaping, formants, filters, and compression.
And if memory or previous-frame data is allowed, possibly even reverb or delay-style effects.

I have some very limited C++ knowledge from introductory programming classes at University. However, I could come up with a proof of concept (as there are so many tools these days to assist) maybe in LADSPA if this would be feasible.

I understand that the team probably has a lot on. However, I believe that that this idea could be a game changer for LMMS.


Re: Idea for Frequency Domain FX Transfer Function Plugin

Posted: Fri Nov 21, 2025 12:30 am
by regulus

That sounds cool! I also once had an idea that maybe it would be cool to have a plugin where you can place poles and zeros of a transfer function. Where basically the user can drag the points around the 2d complex plane and view the resulting frequency/phase response. That would probably be a bit unintuitive though; maybe for educational purposes it would be neat.

Your idea of doing a real FFT + IFFT sounds more powerful. I am curious how you would deal with windowing/artifacts with it though. Afaik FFTs normally create artifacts if used on short segments of audio if they don't perfectly loop, since there's a discontinuity at the end/start. I know sometimes that's alleviated by adding a smooth windowing function which tapers off the ends. But I'm not an expert in that so idk what the proper way to do it is.


Re: Idea for Frequency Domain FX Transfer Function Plugin

Posted: Sun Nov 23, 2025 2:51 am
by ewanpettigrew

Hi Regulus,

Your idea about moving poles and zeros around the complex plane is a great one not just as a tool, but especially as something educational. Even if it isn’t the kind of thing you normally see in a DAW, it would be amazing for people who are learning filter theory.

Over the last couple of days I actually tried to put together a very basic LADSPA plugin on Windows just to check whether my compiler setup was correct, and I ended up giving up after hours of fighting with it. I couldn’t get the required libraries to build, I couldn’t even get all the libraries to install. After that I tried compiling LMMS on Windows so I could just slot a prototype effect directly into the source, but I ran into a ton of issues with Qt. I tried MSYS2/MinGW64, PowerShell and the x64 Native Tools command prompt for VS, and the build still wouldn’t complete.

I then thought about trying to build the most bare bones VST2 plugin instead, but I realised that LMMS only really handles VSTs as instruments, not effects I believe. At that point I had to admit defeat, I’m really not a C++ developer (just someone who has studied a couple of introductory programming courses).

So in hindsight I probably shouldn’t have suggested I’d be able to make a prototype myself. I should have kept things on the conceptual/creative side of the idea.

Thanks for raising the point about artefacts that’s a really important part of the whole thing. Even though I’m not a programmer, I had thought a bit about how to avoid them. I think an STFT overlap add method could avoid the typical FFT windowing discontinuity problem. If the audio was split into overlapping frames, multiplied by a window, and the frames faded in/out, that could keep everything continuous.

Thanks again for the insights.

PS. I did think about installing Linux to build LMMS as this seems just too hard on Windows. However, my computer only has 2 SATA connectors with two drives already, so I have no other options at present.


Re: Idea for Frequency Domain FX Transfer Function Plugin

Posted: Wed Nov 26, 2025 5:58 am
by ewanpettigrew

I have put a rough proof of concept together in MATLAB. I'm not sure if any of the developers have access to MATLAB. However, I have had so much trouble trying to compile the LMMS package on Windows. This should also work on the free GNU Octave. This is the best I can do for now.

  • I believe that this is a concept that could set LMMS apart from other software. I don't know of any VSTs that the user can enter transfer functions to process in the frequency domain (effects). The limits are endless just like Xpressive in the time domain (which gave me the idea).

  • There are a number of preset transfer functions, the ability to enter your own, or a semi random transfer function generator.

  • The demo feeds audio of a square wave or sine wave that can be set as a triad or note, and specific frequency (the other option is impulse train) (the real version would be fed audio from an FX track).

  • The demo performs an FFT, multiplies by the transfer function, then performs an inverse FFT.

  • The demo doesn’t use windowing or STFT (on purpose to show the concept in the simplest possible way).

  • The LMMS plugin could later use STFT + overlap.

Code: Select all

% Frequency Expression MATLAB Proof of Concept
%
% 1/16 note loop at 125 BPM (mono)
% Wave selector = sine, square, impulse train
% Minor triad on off selector
% H(freq) typed in MATLAB syntax
% Rough Presets
% Uses sound() in a loop
% ----------------------------------------------------------

% Initialisation Variables
fs   = 44100;
bpm  = 125;
dur16 = (60/bpm)/4;
N    = round(fs * dur16);

% Figure
hFig = figure('Name','Frequency Expression (Transfer Function) Proof of Concept', ...
              'NumberTitle','off', ...
              'MenuBar','none', ...
              'ToolBar','none', ...
              'Position',[100 100 950 520]);

%% User Controls

uicontrol('Style','text','Parent',hFig, ...
    'String','Base frequency (Hz):', ...
    'HorizontalAlignment','left', ...
    'Position',[20 470 150 20]);

hFreq = uicontrol('Style','edit','Parent',hFig, ...
    'String','220', ...
    'Position',[170 468 80 25]);

hTriad = uicontrol('Style','checkbox','Parent',hFig, ...
    'String','Minor triad (f, f+3, f+7 semitones)', ...
    'Value',0, ...
    'Position',[270 468 260 25]);

uicontrol('Style','text','Parent',hFig, ...
    'String','Waveform:', ...
    'HorizontalAlignment','left', ...
    'Position',[20 435 80 20]);

hWave = uicontrol('Style','popupmenu','Parent',hFig, ...
    'String',{'Sine','Square','Impulse train'}, ...
    'Position',[100 433 150 25]);

uicontrol('Style','text','Parent',hFig, ...
    'String','Impulse = one spike per block (triad ignored).', ...
    'HorizontalAlignment','left', ...
    'Position',[270 435 650 20]);

% Presets
uicontrol('Style','text','Parent',hFig, ...
    'String','Preset:', ...
    'HorizontalAlignment','left', ...
    'Position',[20 400 80 20]);

presetNames = { ...
    'Custom', ...
    'Lowpass', ...
    'Highpass', ...
    'Low Shelf', ...
    'High Shelf', ...
    'Telephone', ...
    'Notch', ...
    'Resonator', ...
    'Comb', ...
    'Chorus', ...
    'Allpass', ...
    'Bitcrusher', ...
    'Spectral', ...
    'Spectral Gate', ...
    'Formants', ...
    'Odd Harmonic Booster', ...
    'Phase Tilt', ...
    'Random'};

hPreset = uicontrol('Style','popupmenu','Parent',hFig, ...
    'String',presetNames, ...
    'Position',[100 398 260 25], ...
    'Callback',@onPreset);

% H(freq) editor
uicontrol('Style','text','Parent',hFig, ...
    'String','Transfer function H(freq) (MATLAB, freq in Hz):', ...
    'HorizontalAlignment','left', ...
    'Position',[20 360 350 20]);

hHedit = uicontrol('Style','edit','Parent',hFig, ...
    'String','1./(1+1j*(freq/400))', ...
    'Max',5,'Min',1, ...
    'HorizontalAlignment','left', ...
    'Position',[20 235 380 120]);

% Start/Stop
hStart = uicontrol('Style','pushbutton','Parent',hFig, ...
    'String','Start Loop', ...
    'FontWeight','bold', ...
    'Position',[20 190 100 35], ...
    'Callback',@onStart);

hStop = uicontrol('Style','pushbutton','Parent',hFig, ...
    'String','Stop', ...
    'FontWeight','bold', ...
    'Position',[140 190 100 35], ...
    'Callback',@onStop);

hStatus = uicontrol('Style','text','Parent',hFig, ...
    'String','Idle.', ...
    'HorizontalAlignment','left', ...
    'Position',[20 160 380 20]);

% Plots
hAxMag = axes('Parent',hFig, ...
              'Units','pixels', ...
              'Position',[430 280 480 210]);
title(hAxMag,'|H(freq)| (dB)');
xlabel(hAxMag,'Frequency (Hz)');
ylabel(hAxMag,'Magnitude (dB)');
grid(hAxMag,'on');

hAxWave = axes('Parent',hFig, ...
               'Units','pixels', ...
               'Position',[430 50 480 200]);
title(hAxWave,'Output waveform (one 1/16 block)');
xlabel(hAxWave,'Time (s)');
ylabel(hAxWave,'Amplitude');
grid(hAxWave,'on');

% Shared state
data.fs         = fs;
data.N          = N;
data.hFreq      = hFreq;
data.hTriad     = hTriad;
data.hWave      = hWave;
data.hHedit     = hHedit;
data.hPreset    = hPreset;
data.hStatus    = hStatus;
data.hAxMag     = hAxMag;
data.hAxWave    = hAxWave;
data.presetNames= presetNames;
data.isLooping  = false;
data.block      = zeros(N,1);

guidata(hFig,data);
hFig.CloseRequestFcn = @onClose;

%% Callbacks

function onStart(~,~)
    data = guidata(hFig);

    % Frequency
    f0 = str2double(get(data.hFreq,'String'));
    if isnan(f0) || f0 <= 0
        set(data.hStatus,'String','Error: base frequency must be positive.');
        return;
    end

    waveformIdx = get(data.hWave,'Value');
    useTriad    = get(data.hTriad,'Value') ~= 0;

    % Generate unprocessed block
    x = generateInputBlock(data.N, data.fs, f0, waveformIdx, useTriad);

    % Get H(freq) expression
    raw = get(data.hHedit,'String');
    if iscell(raw)
        lines = string(raw(:));
    elseif ischar(raw)
        lines = string(cellstr(raw));
    else
        lines = string(raw);
    end
    expr = strjoin(lines, newline);
    expr = char(expr);

    try
        [y, freqPos, Hpos] = applyFreqFX(x, data.fs, expr);
    catch ME
        set(data.hStatus,'String',['Error in transfer function: ' ME.message]);
        return;
    end

    % Normalise
    maxAbs = max(abs(y));
    if maxAbs > 0
        y = 0.9 * y / maxAbs;
    end

    % Redraw plots
    updatePlots(data, x, y, freqPos, Hpos);

    % Store block and start loop
    data.block = y(:);
    if ~data.isLooping
        data.isLooping = true;
        guidata(hFig,data);
        set(data.hStatus,'String','Playing loop… Press Stop to end.');
        startLoop(hFig);
    else
        guidata(hFig,data);
        set(data.hStatus,'String','Updated block while looping.');
    end
end

function startLoop(hFigLocal)
    while ishandle(hFigLocal)
        data = guidata(hFigLocal);
        if ~data.isLooping
            break;
        end

        % Play single block
        clear sound
        sound(data.block, data.fs);

        % Wait for length of block whilst allowing GUI events
        pause(numel(data.block) / data.fs);
        drawnow;  % process the callbacks
    end
    % Ensure sound is stopped at exit of loop
    clear sound
    if ishandle(hFigLocal)
        data = guidata(hFigLocal);
        set(data.hStatus,'String','Stopped.');
        data.isLooping = false;
        guidata(hFigLocal,data);
    end
end

function onStop(~,~)
    data = guidata(hFig);
    data.isLooping = false;
    guidata(hFig,data);
    clear sound
    set(data.hStatus,'String','Stopped.');
end

function onClose(~,~)
    data = guidata(hFig);
    data.isLooping = false;
    guidata(hFig,data);
    clear sound
    delete(hFig);
end

function onPreset(src, ~)
    data = guidata(hFig);
    idx = get(src,'Value');

    if idx == 1
        set(data.hStatus,'String','Custom mode: edit H(freq) manually.');
        guidata(hFig,data);
        return;
    end

    if idx == numel(data.presetNames)
        exprStr = randomWildExpression();
    else
        exprStr = presetExpression(idx);
    end

    lines = splitlines(exprStr);
    set(data.hHedit,'String',cellstr(lines));

    set(data.hStatus,'String',['Preset selected: ' data.presetNames{idx}]);
    guidata(hFig,data);
end
end

%% Generate input block
function x = generateInputBlock(N, fs, f0, waveformIdx, useTriad)
    t = (0:N-1).' / fs;

switch waveformIdx
    case 1  % Sine wave
        if useTriad
            r  = f0;
            m3 = f0 * 2^(3/12);
            p5 = f0 * 2^(7/12);
            x = sin(2*pi*r*t) + sin(2*pi*m3*t) + sin(2*pi*p5*t);
            x = x / 3;
        else
            x = sin(2*pi*f0*t);
        end

    case 2  % Square wave
        if useTriad
            r  = f0;
            m3 = f0 * 2^(3/12);
            p5 = f0 * 2^(7/12);
            x = sign(sin(2*pi*r*t)) + ...
                sign(sin(2*pi*m3*t)) + ...
                sign(sin(2*pi*p5*t));
            x = x / 3;
        else
            x = sign(sin(2*pi*f0*t));
        end

    case 3  % Impulse train
        x = zeros(N,1);
        x(1) = 1;

    otherwise
        x = zeros(N,1);
end
end

%% Apply frequency domain effect
function [y, freqPos, Hpos] = applyFreqFX(x, fs, expr)
    N = length(x);
    Nfft = 2^nextpow2(N);
    X = fft(x, Nfft);

k       = (0:Nfft/2).';
freqPos = k * (fs / Nfft);

Hpos = evaluateUserH(expr, freqPos);

if numel(Hpos) ~= numel(freqPos)
    if isscalar(Hpos)
        Hpos = repmat(Hpos, numel(freqPos), 1);
    else
        oldFreq = linspace(0, fs/2, numel(Hpos));
        Hpos = interp1(oldFreq(:), Hpos(:), freqPos, 'linear', 'extrap');
    end
end

Hpos = Hpos(:);

Hfull = [Hpos; conj(Hpos(end-1:-1:2))];

Y = X .* Hfull;
yFull = ifft(Y, 'symmetric');
y = yFull(1:N);
end

%% Evaluate user's transfer function
function Hpos = evaluateUserH(expr, freqPos)
    freq = freqPos; %#ok<NASGU>

try
    Hpos = eval(expr);
    if ~isempty(Hpos)
        return;
    end
catch
    % fall through
end

Hpos = [];
H    = [];

eval(expr);  

if ~isempty(Hpos)
    return;
end
if ~isempty(H)
    Hpos = H;
    return;
end

Hpos = ones(size(freqPos));
end

%% Update graphs
function updatePlots(data, x, y, freqPos, Hpos) %#ok<INUSD>
    axes(data.hAxMag);
    cla(data.hAxMag);
    magdB = 20*log10(abs(Hpos) + 1e-9);
    plot(data.hAxMag, freqPos, magdB);
    xlabel(data.hAxMag,'Frequency (Hz)');
    ylabel(data.hAxMag,'|H(freq)| (dB)');
    title(data.hAxMag,'Magnitude of H(freq)');
    grid(data.hAxMag,'on');

axes(data.hAxWave);
cla(data.hAxWave);
N = length(y);
t = (0:N-1)/data.fs;
plot(data.hAxWave, t, y);
xlabel(data.hAxWave,'Time (s)');
ylabel(data.hAxWave,'Amplitude');
title(data.hAxWave,'Output waveform (one 1/16 block)');
grid(data.hAxWave,'on');
end
 
%% Demo presets
function expr = presetExpression(idx)
    switch idx
        case 2  % Lowpass
            expr = "fc = 800;" + newline + ...
                   "H = 1 ./ sqrt(1 + (freq/fc).^2);";

    case 3  % Highpass
        expr = "fc = 500;" + newline + ...
               "H = sqrt(1 + (freq/fc).^2);";

    case 4  % Low Shelf
        expr = "fc = 200;" + newline + ...
               "gain = 2; % ~+6 dB" + newline + ...
               "H = 1 + (gain-1) ./ (1 + (freq/fc).^2);";

    case 5  % High Shelf
        expr = "fc = 2000;" + newline + ...
               "gain = 2;" + newline + ...
               "H = 1 + (gain-1) ./ (1 + (fc./(freq+1)).^2);";

    case 6  % Telephone
        expr = "H = double(freq>300 & freq<3000);";

    case 7  % Notch Hole
        expr = "fc = 600;" + newline + ...
               "bw = 40;" + newline + ...
               "H = 1 - exp(-((freq-fc).^2)/(2*(bw^2)));";

    case 8  % Resonator
        expr = "fc = 900;" + newline + ...
               "Q = 20;" + newline + ...
               "H = 1 + 8 .* exp(-(Q*(freq-fc)/fc).^2);";

    case 9  % Comb
        expr = "delay = 0.002; % 2 ms" + newline + ...
               "H = 1 + exp(-1j*2*pi*freq*delay);";

    case 10 % Comb Notch
        expr = "delay = 0.003;" + newline + ...
               "H = 1 - exp(-1j*2*pi*freq*delay);";

    case 11 % Allpass
        expr = "fc = 1200;" + newline + ...
               "H = (1 - 1j*(freq/fc)) ./ (1 + 1j*(freq/fc));";

    case 12 % Bitcrusher
        expr = "H = (mod(freq,500) < 250) * 2 - 1;";

    case 13 % Spectral
        expr = "H = exp(-0.0004*freq);";

    case 14 % Spectral Gate Sweep
        expr = "H = (freq > 500) .* exp(-0.0008*(freq-500)) .* sin(freq*0.005);";

    case 15 % Formants
        expr = "f1=800; f2=1500; f3=2500;" + newline + ...
               "H = exp(-((freq-f1).^2)/(2*80^2)) + ..." + newline + ...
               "    exp(-((freq-f2).^2)/(2*150^2)) + ..." + newline + ...
               "    exp(-((freq-f3).^2)/(2*200^2));";

    case 16 % Odd Harmonic Booster
        expr = "H = 1 + 0.8 * sin(pi*freq/200);";

    case 17 % Phase Tilt
        expr = "H = exp(1j * 0.0005 * freq);";

    otherwise
        expr = "H = ones(size(freq));";
end
end

%% Semi-Random transfer function
% Select one of 7 categories randomly, then randomly generate the details
% inside
function expr = randomWildExpression()
    mode = randi(7);

switch mode
    case 1  % Random resonant
        numPeaks = randi([2 6]);
        centers = sort(200 + rand(numPeaks,1)*8000);
        widths  = 40 + rand(numPeaks,1)*400;
        gains   = 0.5 + rand(numPeaks,1)*5;
        expr = "H = 0;";
        for i = 1:numPeaks
            expr = expr + newline + ...
                sprintf("H = H + %0.3f * exp(-((freq-%0.1f).^2)/(2*(%0.1f)^2));", ...
                    gains(i), centers(i), widths(i));
        end

    case 2  % Random comb
        delay = 0.0005 + rand()*0.01;
        taps  = randi([2 6]);
        expr  = "H = zeros(size(freq));";
        for i = 1:taps
            expr = expr + newline + ...
                sprintf("H = H + exp(-1j*2*pi*freq*(%f)*%d);", delay, i);
        end

    case 3  % Spectral fractal
        scale = 0.0001 + rand()*0.001;
        expr  = sprintf("H = abs(sin(freq*%f)).^0.8 .* exp(-%f*freq);", ...
                        scale, scale*4);
        expr  = string(expr);

    case 4  % Bump comb tilt
        fc = 200 + rand()*4000;
        bw = 50 + rand()*500;
        delay = 0.001 + rand()*0.005;
        expr = sprintf([ ...
            "H = exp(-((freq-%f).^2)/(2*(%f)^2));\n" ...
            "H = H + 0.8*(1 + exp(-1j*2*pi*freq*(%f)));\n" ...
            "H = H .* exp(-0.0003*freq);" ], fc, bw, delay);
        expr = string(expr);

    case 5  % Spectral clusters
        numBands = randi([3 10]);
        centers = sort(100 + rand(numBands,1)*8000);
        expr = "H = zeros(size(freq));";
        for i = 1:numBands
            expr = expr + newline + ...
                sprintf("H = H + double(abs(freq-%0.1f) < %0.1f);", ...
                centers(i), 20 + 80*rand());
        end
        expr = expr + newline + "H = H .* exp(-0.0002*freq);";

    case 6  % FM phase warp notch
        rate = 0.002 + rand()*0.01;
        expr = sprintf([ ...
            "H = exp(1j*sin(freq*%f));\n" ...
            "H = H .* (1 - exp(-1j*2*pi*freq*(%f)));" ], rate, rate*4);
        expr = string(expr);

    case 7  % Spectral folding 
        fold = 300 + rand()*2000;
        expr = sprintf([ ...
            "H = abs(mod(freq,%f) - %f/2);\n" ...
            "H = H ./ max(H + 1e-6);\n" ...
            "H = H.^(-1.2);" ], fold, fold);
        expr = string(expr);
end
end