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